diff --git a/doc/api/figure_api.rst b/doc/api/figure_api.rst index 3c1deb9fdc3c..1beb0a701b26 100644 --- a/doc/api/figure_api.rst +++ b/doc/api/figure_api.rst @@ -5,26 +5,5 @@ .. currentmodule:: matplotlib.figure .. automodule:: matplotlib.figure - :no-members: - :no-inherited-members: - -Classes -------- - -.. autosummary:: - :toctree: _as_gen/ - :template: autosummary.rst - :nosignatures: - - Figure - SubplotParams - -Functions ---------- - -.. autosummary:: - :toctree: _as_gen/ - :template: autosummary.rst - :nosignatures: - - figaspect + :members: + :inherited-members: diff --git a/doc/api/next_api_changes/development/18356-JMK.rst b/doc/api/next_api_changes/development/18356-JMK.rst new file mode 100644 index 000000000000..14d567c1fa38 --- /dev/null +++ b/doc/api/next_api_changes/development/18356-JMK.rst @@ -0,0 +1,17 @@ +FigureBase class added, and Figure class made a child +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The new subfigure feature motivated some re-organization of the +`.figure.Figure` class, so that the new `.figure.SubFigure` class could have +all the capabilities of a figure. + +The `.figure.Figure` class is now a subclass of `.figure.FigureBase`, where +`.figure.FigureBase` contains figure-level artist addition routines, and +the `.figure.Figure` subclass just contains features that are unique to the +outer figure. + +Note that there is a new *transSubfigure* transform +associated with the subfigure. This transform also exists for a +`.Figure` instance, and is equal to *transFigure* in that case, +so code that uses the transform stack that wants to place objects on either +the parent figure or one of the subfigures should use *transSubfigure*. diff --git a/doc/users/next_whats_new/subfigures.rst b/doc/users/next_whats_new/subfigures.rst new file mode 100644 index 000000000000..3290afa460f4 --- /dev/null +++ b/doc/users/next_whats_new/subfigures.rst @@ -0,0 +1,53 @@ +New subfigure functionality +--------------------------- +New `.figure.Figure.add_subfigure` and `.figure.Figure.subfigures` +functionalities allow creating virtual figures within figures. Similar +nesting was previously done with nested gridspecs +( see :doc:`/gallery/subplots_axes_and_figures/gridspec_nested`). However, this +did not allow localized figure artists (i.e. a colorbar or suptitle) that +only pertained to each subgridspec. + +The new methods `.figure.Figure.add_subfigure` and `.figure.Figure.subfigures` +are meant to rhyme with `.figure.Figure.add_subplot` and +`.figure.Figure.subplots` and have most of the same arguments. + +See :doc:`/gallery/subplots_axes_and_figures/subfigures`. + +.. note:: + + The subfigure functionality is experimental API as of v3.4. + +.. plot:: + + def example_plot(ax, fontsize=12, hide_labels=False): + pc = ax.pcolormesh(np.random.randn(30, 30)) + 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 pc + + np.random.seed(19680808) + fig = plt.figure(constrained_layout=True, figsize=(10, 4)) + subfigs = fig.subfigures(1, 2, wspace=0.07) + + axsLeft = subfigs[0].subplots(1, 2, sharey=True) + subfigs[0].set_facecolor('0.75') + for ax in axsLeft: + pc = example_plot(ax) + subfigs[0].suptitle('Left plots', fontsize='x-large') + subfigs[0].colorbar(pc, shrink=0.6, ax=axsLeft, location='bottom') + + axsRight = subfigs[1].subplots(3, 1, sharex=True) + for nn, ax in enumerate(axsRight): + pc = example_plot(ax, hide_labels=True) + if nn == 2: + ax.set_xlabel('xlabel') + if nn == 1: + ax.set_ylabel('ylabel') + subfigs[1].colorbar(pc, shrink=0.6, ax=axsRight) + subfigs[1].suptitle('Right plots', fontsize='x-large') + + fig.suptitle('Figure suptitle', fontsize='xx-large') + + plt.show() diff --git a/examples/subplots_axes_and_figures/gridspec_nested.py b/examples/subplots_axes_and_figures/gridspec_nested.py index 20c211689e68..e47106e5d47c 100644 --- a/examples/subplots_axes_and_figures/gridspec_nested.py +++ b/examples/subplots_axes_and_figures/gridspec_nested.py @@ -6,6 +6,10 @@ GridSpecs can be nested, so that a subplot from a parent GridSpec can set the position for a nested grid of subplots. +Note that the same functionality can be achieved more directly with +`~.figure.FigureBase.subfigures`; see +:doc:`/gallery/subplots_axes_and_figures/subfigures`. + """ import matplotlib.pyplot as plt import matplotlib.gridspec as gridspec diff --git a/examples/subplots_axes_and_figures/subfigures.py b/examples/subplots_axes_and_figures/subfigures.py new file mode 100644 index 000000000000..e0f56618754f --- /dev/null +++ b/examples/subplots_axes_and_figures/subfigures.py @@ -0,0 +1,148 @@ +""" +================= +Figure subfigures +================= + +Sometimes it is desirable to have a figure with two different layouts in it. +This can be achieved with +:doc:`nested gridspecs`, +but having a virtual figure with its own artists is helpful, so +Matplotlib also has "subfigures", accessed by calling +`matplotlib.figure.Figure.add_subfigure` in a way that is analagous to +`matplotlib.figure.Figure.add_subplot`, or +`matplotlib.figure.Figure.subfigures` to make an array of subfigures. Note +that subfigures can also have their own child subfigures. + +.. note:: + ``subfigure`` is new in v3.4, and the API is still provisional. + +""" +import matplotlib.pyplot as plt +import numpy as np + + +def example_plot(ax, fontsize=12, hide_labels=False): + pc = ax.pcolormesh(np.random.randn(30, 30), vmin=-2.5, vmax=2.5) + 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 pc + +np.random.seed(19680808) +# gridspec inside gridspec +fig = plt.figure(constrained_layout=True, figsize=(10, 4)) +subfigs = fig.subfigures(1, 2, wspace=0.07) + +axsLeft = subfigs[0].subplots(1, 2, sharey=True) +subfigs[0].set_facecolor('0.75') +for ax in axsLeft: + pc = example_plot(ax) +subfigs[0].suptitle('Left plots', fontsize='x-large') +subfigs[0].colorbar(pc, shrink=0.6, ax=axsLeft, location='bottom') + +axsRight = subfigs[1].subplots(3, 1, sharex=True) +for nn, ax in enumerate(axsRight): + pc = example_plot(ax, hide_labels=True) + if nn == 2: + ax.set_xlabel('xlabel') + if nn == 1: + ax.set_ylabel('ylabel') + +subfigs[1].set_facecolor('0.85') +subfigs[1].colorbar(pc, shrink=0.6, ax=axsRight) +subfigs[1].suptitle('Right plots', fontsize='x-large') + +fig.suptitle('Figure suptitle', fontsize='xx-large') + +plt.show() + +############################################################################## +# It is possible to mix subplots and subfigures using +# `matplotlib.figure.Figure.add_subfigure`. This requires getting +# the gridspec that the subplots are laid out on. + +fig, axs = plt.subplots(2, 3, constrained_layout=True, figsize=(10, 4)) +gridspec = axs[0, 0].get_subplotspec().get_gridspec() + +# clear the left column for the subfigure: +for a in axs[:, 0]: + a.remove() + +# plot data in remaining axes: +for a in axs[:, 1:].flat: + a.plot(np.arange(10)) + +# make the subfigure in the empy gridspec slots: +subfig = fig.add_subfigure(gridspec[:, 0]) + +axsLeft = subfig.subplots(1, 2, sharey=True) +subfig.set_facecolor('0.75') +for ax in axsLeft: + pc = example_plot(ax) +subfig.suptitle('Left plots', fontsize='x-large') +subfig.colorbar(pc, shrink=0.6, ax=axsLeft, location='bottom') + +fig.suptitle('Figure suptitle', fontsize='xx-large') +plt.show() + +############################################################################## +# Subfigures can have different widths and heights. This is exactly the +# same example as the first example, but *width_ratios* has been changed: + +fig = plt.figure(constrained_layout=True, figsize=(10, 4)) +subfigs = fig.subfigures(1, 2, wspace=0.07, width_ratios=[2, 1]) + +axsLeft = subfigs[0].subplots(1, 2, sharey=True) +subfigs[0].set_facecolor('0.75') +for ax in axsLeft: + pc = example_plot(ax) +subfigs[0].suptitle('Left plots', fontsize='x-large') +subfigs[0].colorbar(pc, shrink=0.6, ax=axsLeft, location='bottom') + +axsRight = subfigs[1].subplots(3, 1, sharex=True) +for nn, ax in enumerate(axsRight): + pc = example_plot(ax, hide_labels=True) + if nn == 2: + ax.set_xlabel('xlabel') + if nn == 1: + ax.set_ylabel('ylabel') + +subfigs[1].set_facecolor('0.85') +subfigs[1].colorbar(pc, shrink=0.6, ax=axsRight) +subfigs[1].suptitle('Right plots', fontsize='x-large') + +fig.suptitle('Figure suptitle', fontsize='xx-large') + +plt.show() + +############################################################################## +# Subfigures can be also be nested: + +fig = plt.figure(constrained_layout=True, figsize=(10, 8)) + +fig.suptitle('fig') + +subfigs = fig.subfigures(1, 2, wspace=0.07) + +subfigs[0].set_facecolor('coral') +subfigs[0].suptitle('subfigs[0]') + +subfigs[1].set_facecolor('coral') +subfigs[1].suptitle('subfigs[1]') + +subfigsnest = subfigs[0].subfigures(2, 1, height_ratios=[1, 1.4]) +subfigsnest[0].suptitle('subfigsnest[0]') +subfigsnest[0].set_facecolor('r') +axsnest0 = subfigsnest[0].subplots(1, 2, sharey=True) +for nn, ax in enumerate(axsnest0): + pc = example_plot(ax, hide_labels=True) +subfigsnest[0].colorbar(pc, ax=axsnest0) + +subfigsnest[1].suptitle('subfigsnest[1]') +subfigsnest[1].set_facecolor('g') +axsnest1 = subfigsnest[1].subplots(3, 1, sharex=True) + +axsRight = subfigs[1].subplots(2, 2) + +plt.show() diff --git a/examples/subplots_axes_and_figures/subplots_demo.py b/examples/subplots_axes_and_figures/subplots_demo.py index 522275decf1e..bbc8afa469fa 100644 --- a/examples/subplots_axes_and_figures/subplots_demo.py +++ b/examples/subplots_axes_and_figures/subplots_demo.py @@ -144,7 +144,7 @@ # Still there remains an unused empty space between the subplots. # # To precisely control the positioning of the subplots, one can explicitly -# create a `.GridSpec` with `.add_gridspec`, and then call its +# create a `.GridSpec` with `.Figure.add_gridspec`, and then call its # `~.GridSpecBase.subplots` method. For example, we can reduce the height # between vertical subplots using ``add_gridspec(hspace=0)``. # diff --git a/lib/matplotlib/_constrained_layout.py b/lib/matplotlib/_constrained_layout.py index 60fc4e0d5c8b..181b8d70387c 100644 --- a/lib/matplotlib/_constrained_layout.py +++ b/lib/matplotlib/_constrained_layout.py @@ -103,11 +103,10 @@ def do_constrained_layout(fig, renderer, h_pad, w_pad, # larger/smaller). This second reposition tends to be much milder, # so doing twice makes things work OK. - # make margins for all the axes and subpanels in the + # make margins for all the axes and subfigures in the # figure. Add margins for colorbars... _make_layout_margins(fig, renderer, h_pad=h_pad, w_pad=w_pad, hspace=hspace, wspace=wspace) - _make_margin_suptitles(fig, renderer, h_pad=h_pad, w_pad=w_pad) # if a layout is such that a columns (or rows) margin has no @@ -132,7 +131,7 @@ def _check_no_collapsed_axes(fig): """ Check that no axes have collapsed to zero size. """ - for panel in fig.panels: + for panel in fig.subfigs: ok = _check_no_collapsed_axes(panel) if not ok: return False @@ -198,7 +197,7 @@ def _make_layout_margins(fig, renderer, *, w_pad=0, h_pad=0, Then make room for colorbars. """ - for panel in fig.panels: # recursively make child panel margins + for panel in fig.subfigs: # recursively make child panel margins ss = panel._subplotspec _make_layout_margins(panel, renderer, w_pad=w_pad, h_pad=h_pad, hspace=hspace, wspace=wspace) @@ -207,8 +206,7 @@ def _make_layout_margins(fig, renderer, *, w_pad=0, h_pad=0, hspace=hspace, wspace=wspace) panel._layoutgrid.parent.edit_outer_margin_mins(margins, ss) - # for ax in [a for a in fig._localaxes if hasattr(a, 'get_subplotspec')]: - for ax in fig.get_axes(): + for ax in fig._localaxes.as_list(): if not hasattr(ax, 'get_subplotspec') or not ax.get_in_layout(): continue @@ -223,7 +221,6 @@ def _make_layout_margins(fig, renderer, *, w_pad=0, h_pad=0, hspace=hspace, wspace=wspace) margin0 = margin.copy() pos, bbox = _get_pos_and_bbox(ax, renderer) - # the margin is the distance between the bounding box of the axes # and its position (plus the padding from above) margin['left'] += pos.x0 - bbox.x0 @@ -279,13 +276,17 @@ def _make_layout_margins(fig, renderer, *, w_pad=0, h_pad=0, def _make_margin_suptitles(fig, renderer, *, w_pad=0, h_pad=0): # Figure out how large the suptitle is and make the # top level figure margin larger. - for panel in fig.panels: + for panel in fig.subfigs: _make_margin_suptitles(panel, renderer, w_pad=w_pad, h_pad=h_pad) - invTransFig = fig.transPanel.inverted().transform_bbox - - w_pad, h_pad = (fig.transPanel - fig.transFigure).transform((w_pad, h_pad)) if fig._suptitle is not None and fig._suptitle.get_in_layout(): + invTransFig = fig.transSubfigure.inverted().transform_bbox + parenttrans = fig.transFigure + w_pad, h_pad = (fig.transSubfigure - + parenttrans).transform((w_pad, 1 - h_pad)) + w_pad, one = (fig.transSubfigure - + parenttrans).transform((w_pad, 1)) + h_pad = one - h_pad bbox = invTransFig(fig._suptitle.get_tightbbox(renderer)) p = fig._suptitle.get_position() fig._suptitle.set_position((p[0], 1-h_pad)) @@ -317,7 +318,7 @@ def _match_submerged_margins(fig): See test_constrained_layout::test_constrained_layout12 for an example. """ - for panel in fig.panels: + for panel in fig.subfigs: _match_submerged_margins(panel) axs = [a for a in fig.get_axes() if (hasattr(a, 'get_subplotspec') @@ -429,7 +430,7 @@ def _get_pos_and_bbox(ax, renderer): fig = ax.figure pos = ax.get_position(original=True) # pos is in panel co-ords, but we need in figure for the layout - pos = pos.transformed(fig.transPanel - fig.transFigure) + pos = pos.transformed(fig.transSubfigure - fig.transFigure) try: tightbbox = ax.get_tightbbox(renderer=renderer, for_layout_only=True) except TypeError: @@ -446,18 +447,16 @@ def _reposition_axes(fig, renderer, *, w_pad=0, h_pad=0, hspace=0, wspace=0): """ Reposition all the axes based on the new inner bounding box. """ - trans_fig_to_panel = fig.transFigure - fig.transPanel - for panel in fig.panels: - bbox = panel._layoutgrid.get_outer_bbox() - panel._redo_transform_rel_fig( - bbox=bbox.transformed(trans_fig_to_panel)) - _reposition_axes(panel, renderer, + trans_fig_to_subfig = fig.transFigure - fig.transSubfigure + for sfig in fig.subfigs: + bbox = sfig._layoutgrid.get_outer_bbox() + sfig._redo_transform_rel_fig( + bbox=bbox.transformed(trans_fig_to_subfig)) + _reposition_axes(sfig, renderer, w_pad=w_pad, h_pad=h_pad, wspace=wspace, hspace=hspace) - # for ax in fig._localaxes: - # if not hasattr(a, 'get_subplotspec'): - for ax in fig.get_axes(): + for ax in fig._localaxes.as_list(): if not hasattr(ax, 'get_subplotspec') or not ax.get_in_layout(): continue @@ -475,7 +474,7 @@ def _reposition_axes(fig, renderer, *, w_pad=0, h_pad=0, hspace=0, wspace=0): cols=ss.colspan) # transform from figure to panel for set_position: - newbbox = trans_fig_to_panel.transform_bbox(bbox) + newbbox = trans_fig_to_subfig.transform_bbox(bbox) ax._set_position(newbbox) # move the colorbars: @@ -512,7 +511,7 @@ def _reposition_colorbar(cbax, renderer, *, offset=None): gs = parents[0].get_gridspec() ncols, nrows = gs.ncols, gs.nrows fig = cbax.figure - trans_fig_to_panel = fig.transFigure - fig.transPanel + trans_fig_to_subfig = fig.transFigure - fig.transSubfigure cb_rspans, cb_cspans = _get_cb_parent_spans(cbax) bboxparent = gs._layoutgrid.get_bbox_for_cb(rows=cb_rspans, cols=cb_cspans) @@ -562,8 +561,8 @@ def _reposition_colorbar(cbax, renderer, *, offset=None): offset['bottom'] += cbbbox.height + cbpad pbcb = pbcb.translated(0, dy) - pbcb = trans_fig_to_panel.transform_bbox(pbcb) - cbax.set_transform(fig.transPanel) + pbcb = trans_fig_to_subfig.transform_bbox(pbcb) + cbax.set_transform(fig.transSubfigure) cbax._set_position(pbcb) cbax.set_aspect(aspect, anchor=anchor, adjustable='box') return offset @@ -576,7 +575,7 @@ def _reset_margins(fig): Margins are usually set as a minimum, so if the figure gets smaller the minimum needs to be zero in order for it to grow again. """ - for span in fig.panels: + for span in fig.subfigs: _reset_margins(span) for ax in fig.axes: if hasattr(ax, 'get_subplotspec') and ax.get_in_layout(): diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 58348483cac9..42e071e4c477 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -449,7 +449,7 @@ def indicate_inset(self, bounds, inset_ax=None, *, transform=None, # decide which two of the lines to keep visible.... pos = inset_ax.get_position() - bboxins = pos.transformed(self.figure.transFigure) + bboxins = pos.transformed(self.figure.transSubfigure) rectbbox = mtransforms.Bbox.from_bounds( *bounds ).transformed(transform) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 49eee82679a0..8bb5545d8a7e 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -673,7 +673,7 @@ def set_figure(self, fig): super().set_figure(fig) self.bbox = mtransforms.TransformedBbox(self._position, - fig.transFigure) + fig.transSubfigure) # these will be updated later as data is added self.dataLim = mtransforms.Bbox.null() self._viewLim = mtransforms.Bbox.unit() @@ -1652,8 +1652,10 @@ def apply_aspect(self, position=None): self._set_position(position, which='active') return - fig_width, fig_height = self.get_figure().get_size_inches() - fig_aspect = fig_height / fig_width + trans = self.get_figure().transSubfigure + bb = mtransforms.Bbox.from_bounds(0, 0, 1, 1).transformed(trans) + # this is the physical aspect of the panel (or figure): + fig_aspect = bb.height / bb.width if self._adjustable == 'box': if self in self._twinned_axes: diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 04b3731d6d52..2e3d9a3100d0 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -3,6 +3,12 @@ `Figure` Top level `~matplotlib.artist.Artist`, which holds all plot elements. + Many methods are implemented in `FigureBase`. + +`SubFigure` + A logical figure inside a figure, usually added to a figure (or parent + `SubFigure`) with `Figure.add_subfigure` or `Figure.subfigures` methods + (provisional API v3.4). `SubplotParams` Control the default spacing between subplots. @@ -213,149 +219,23 @@ def update(self, left=None, bottom=None, right=None, top=None, self.hspace = hspace -class Figure(Artist): +class FigureBase(Artist): """ - The top level container for all the plot elements. - - The Figure instance supports callbacks through a *callbacks* attribute - which is a `.CallbackRegistry` instance. The events you can connect to - are 'dpi_changed', and the callback will be called with ``func(fig)`` where - fig is the `Figure` instance. - - Attributes - ---------- - patch - The `.Rectangle` instance representing the figure background patch. - - suppressComposite - For multiple figure images, the figure will make composite images - depending on the renderer option_image_nocomposite function. If - *suppressComposite* is a boolean, this will override the renderer. + Base class for `.figure.Figure` and `.figure.SubFigure` containing the + methods that add artists to the figure or subfigure, create axes, etc. """ - - def __str__(self): - return "Figure(%gx%g)" % tuple(self.bbox.size) - - def __repr__(self): - return "<{clsname} size {h:g}x{w:g} with {naxes} Axes>".format( - clsname=self.__class__.__name__, - h=self.bbox.size[0], w=self.bbox.size[1], - naxes=len(self.axes), - ) - - def __init__(self, - figsize=None, - dpi=None, - facecolor=None, - edgecolor=None, - linewidth=0.0, - frameon=None, - subplotpars=None, # rc figure.subplot.* - tight_layout=None, # rc figure.autolayout - constrained_layout=None, # rc figure.constrained_layout.use - ): - """ - Parameters - ---------- - figsize : 2-tuple of floats, default: :rc:`figure.figsize` - Figure dimension ``(width, height)`` in inches. - - dpi : float, default: :rc:`figure.dpi` - Dots per inch. - - facecolor : default: :rc:`figure.facecolor` - The figure patch facecolor. - - edgecolor : default: :rc:`figure.edgecolor` - The figure patch edge color. - - linewidth : float - The linewidth of the frame (i.e. the edge linewidth of the figure - patch). - - frameon : bool, default: :rc:`figure.frameon` - If ``False``, suppress drawing the figure background patch. - - subplotpars : `SubplotParams` - Subplot parameters. If not given, the default subplot - parameters :rc:`figure.subplot.*` are used. - - tight_layout : bool or dict, default: :rc:`figure.autolayout` - If ``False`` use *subplotpars*. If ``True`` adjust subplot - parameters using `.tight_layout` with default padding. - When providing a dict containing the keys ``pad``, ``w_pad``, - ``h_pad``, and ``rect``, the default `.tight_layout` paddings - will be overridden. - - constrained_layout : bool, default: :rc:`figure.constrained_layout.use` - If ``True`` use constrained layout to adjust positioning of plot - elements. Like ``tight_layout``, but designed to be more - flexible. See - :doc:`/tutorials/intermediate/constrainedlayout_guide` - for examples. (Note: does not work with `add_subplot` or - `~.pyplot.subplot2grid`.) - """ + def __init__(self): super().__init__() # remove the non-figure artist _axes property # as it makes no sense for a figure to be _in_ an axes # this is used by the property methods in the artist base class # which are over-ridden in this class del self._axes - self.callbacks = cbook.CallbackRegistry() - - if figsize is None: - figsize = mpl.rcParams['figure.figsize'] - if dpi is None: - dpi = mpl.rcParams['figure.dpi'] - if facecolor is None: - facecolor = mpl.rcParams['figure.facecolor'] - if edgecolor is None: - edgecolor = mpl.rcParams['figure.edgecolor'] - if frameon is None: - frameon = mpl.rcParams['figure.frameon'] - - if not np.isfinite(figsize).all() or (np.array(figsize) < 0).any(): - raise ValueError('figure size must be positive finite not ' - f'{figsize}') - self.bbox_inches = Bbox.from_bounds(0, 0, *figsize) - - self.dpi_scale_trans = Affine2D().scale(dpi) - # do not use property as it will trigger - self._dpi = dpi - self.bbox = TransformedBbox(self.bbox_inches, self.dpi_scale_trans) - - self.transFigure = BboxTransformTo(self.bbox) - - self.patch = Rectangle( - xy=(0, 0), width=1, height=1, visible=frameon, - facecolor=facecolor, edgecolor=edgecolor, linewidth=linewidth, - # Don't let the figure patch influence bbox calculation. - in_layout=False) - self._set_artist_props(self.patch) - self.patch.set_antialiased(False) - FigureCanvasBase(self) # Set self.canvas. self._suptitle = None - if subplotpars is None: - subplotpars = SubplotParams() - - self.subplotpars = subplotpars - # constrained_layout: self._layoutgrid = None - self._constrained = False - - self.set_tight_layout(tight_layout) - - self._axstack = _AxesStack() # track all figure axes and current axes - self.clf() - self._cachedRenderer = None - - self.set_constrained_layout(constrained_layout) - # stub for subpanels: - self.panels = [] - self.transPanel = self.transFigure # groupers to keep track of x and y labels we want to align. # see self.align_xlabels and self.align_ylabels and @@ -363,285 +243,115 @@ def __init__(self, self._align_xlabel_grp = cbook.Grouper() self._align_ylabel_grp = cbook.Grouper() + self.figure = self # list of child gridspecs for this figure self._gridspecs = [] + self._localaxes = _AxesStack() # keep track of axes at this level + self.artists = [] + self.lines = [] + self.patches = [] + self.texts = [] + self.images = [] + self.legends = [] + self.subfigs = [] + self._suptitle = None + self.stale = True + self.suppressComposite = None - # TODO: I'd like to dynamically add the _repr_html_ method - # to the figure in the right context, but then IPython doesn't - # use it, for some reason. - - def _repr_html_(self): - # We can't use "isinstance" here, because then we'd end up importing - # webagg unconditionally. - if 'WebAgg' in type(self.canvas).__name__: - from matplotlib.backends import backend_webagg - return backend_webagg.ipython_inline_display(self) - - def show(self, warn=True): - """ - If using a GUI backend with pyplot, display the figure window. - - If the figure was not created using `~.pyplot.figure`, it will lack - a `~.backend_bases.FigureManagerBase`, and this method will raise an - AttributeError. - - .. warning:: + def _get_draw_artists(self, renderer): + """Also runs apply_aspect""" + artists = self.get_children() + for sfig in self.subfigs: + artists.remove(sfig) + childa = sfig.get_children() + for child in childa: + if child in artists: + artists.remove(child) - This does not manage an GUI event loop. Consequently, the figure - may only be shown briefly or not shown at all if you or your - environment are not managing an event loop. + artists.remove(self.patch) + artists = sorted( + (artist for artist in artists if not artist.get_animated()), + key=lambda artist: artist.get_zorder()) + for ax in self._localaxes.as_list(): + locator = ax.get_axes_locator() + if locator: + pos = locator(ax, renderer) + ax.apply_aspect(pos) + else: + ax.apply_aspect() - Proper use cases for `.Figure.show` include running this from a - GUI application or an IPython shell. + for child in ax.get_children(): + if hasattr(child, 'apply_aspect'): + locator = child.get_axes_locator() + if locator: + pos = locator(child, renderer) + child.apply_aspect(pos) + else: + child.apply_aspect() + return artists - If you're running a pure python shell or executing a non-GUI - python script, you should use `matplotlib.pyplot.show` instead, - which takes care of managing the event loop for you. + def autofmt_xdate( + self, bottom=0.2, rotation=30, ha='right', which='major'): + """ + Date ticklabels often overlap, so it is useful to rotate them + and right align them. Also, a common use case is a number of + subplots with shared xaxes where the x-axis is date data. The + ticklabels are often long, and it helps to rotate them on the + bottom subplot and turn them off on other subplots, as well as + turn off xlabels. Parameters ---------- - warn : bool, default: True - If ``True`` and we are not running headless (i.e. on Linux with an - unset DISPLAY), issue warning when called on a non-GUI backend. + bottom : float, default: 0.2 + The bottom of the subplots for `subplots_adjust`. + rotation : float, default: 30 degrees + The rotation angle of the xtick labels in degrees. + ha : {'left', 'center', 'right'}, default: 'right' + The horizontal alignment of the xticklabels. + which : {'major', 'minor', 'both'}, default: 'major' + Selects which ticklabels to rotate. """ - if self.canvas.manager is None: - raise AttributeError( - "Figure.show works only for figures managed by pyplot, " - "normally created by pyplot.figure()") - try: - self.canvas.manager.show() - except NonGuiException as exc: - cbook._warn_external(str(exc)) + if which is None: + cbook.warn_deprecated( + "3.3", message="Support for passing which=None to mean " + "which='major' is deprecated since %(since)s and will be " + "removed %(removal)s.") + allsubplots = all(hasattr(ax, 'get_subplotspec') for ax in self.axes) + if len(self.axes) == 1: + for label in self.axes[0].get_xticklabels(which=which): + label.set_ha(ha) + label.set_rotation(rotation) + else: + if allsubplots: + for ax in self.get_axes(): + if ax.get_subplotspec().is_last_row(): + for label in ax.get_xticklabels(which=which): + label.set_ha(ha) + label.set_rotation(rotation) + else: + for label in ax.get_xticklabels(which=which): + label.set_visible(False) + ax.set_xlabel('') - def get_axes(self): - """ - Return a list of axes in the Figure. You can access and modify the - axes in the Figure through this list. + if allsubplots: + self.subplots_adjust(bottom=bottom) + self.stale = True - Do not modify the list itself. Instead, use `~Figure.add_axes`, - `~.Figure.add_subplot` or `~.Figure.delaxes` to add or remove an axes. + def get_children(self): + """Get a list of artists contained in the figure.""" + return [self.patch, + *self.artists, + *self._localaxes.as_list(), + *self.lines, + *self.patches, + *self.texts, + *self.images, + *self.legends, + *self.subfigs] - Note: This is equivalent to the property `~.Figure.axes`. + def contains(self, mouseevent): """ - return self._axstack.as_list() - - axes = property(get_axes, doc=""" - List of axes in the Figure. You can access and modify the axes in the - Figure through this list. - - Do not modify the list itself. Instead, use "`~Figure.add_axes`, - `~.Figure.add_subplot` or `~.Figure.delaxes` to add or remove an axes. - """) - - def _get_dpi(self): - return self._dpi - - def _set_dpi(self, dpi, forward=True): - """ - Parameters - ---------- - dpi : float - - forward : bool - Passed on to `~.Figure.set_size_inches` - """ - if dpi == self._dpi: - # We don't want to cause undue events in backends. - return - self._dpi = dpi - self.dpi_scale_trans.clear().scale(dpi) - w, h = self.get_size_inches() - self.set_size_inches(w, h, forward=forward) - self.callbacks.process('dpi_changed', self) - - dpi = property(_get_dpi, _set_dpi, doc="The resolution in dots per inch.") - - def get_tight_layout(self): - """Return whether `.tight_layout` is called when drawing.""" - return self._tight - - def set_tight_layout(self, tight): - """ - Set whether and how `.tight_layout` is called when drawing. - - Parameters - ---------- - tight : bool or dict with keys "pad", "w_pad", "h_pad", "rect" or None - If a bool, sets whether to call `.tight_layout` upon drawing. - If ``None``, use the ``figure.autolayout`` rcparam instead. - If a dict, pass it as kwargs to `.tight_layout`, overriding the - default paddings. - """ - if tight is None: - tight = mpl.rcParams['figure.autolayout'] - self._tight = bool(tight) - self._tight_parameters = tight if isinstance(tight, dict) else {} - self.stale = True - - def get_constrained_layout(self): - """ - Return whether constrained layout is being used. - - See :doc:`/tutorials/intermediate/constrainedlayout_guide`. - """ - return self._constrained - - def set_constrained_layout(self, constrained): - """ - Set whether ``constrained_layout`` is used upon drawing. If None, - :rc:`figure.constrained_layout.use` value will be used. - - When providing a dict containing the keys `w_pad`, `h_pad` - the default ``constrained_layout`` paddings will be - overridden. These pads are in inches and default to 3.0/72.0. - ``w_pad`` is the width padding and ``h_pad`` is the height padding. - - See :doc:`/tutorials/intermediate/constrainedlayout_guide`. - - Parameters - ---------- - constrained : bool or dict or None - """ - self._constrained_layout_pads = dict() - self._constrained_layout_pads['w_pad'] = None - self._constrained_layout_pads['h_pad'] = None - self._constrained_layout_pads['wspace'] = None - self._constrained_layout_pads['hspace'] = None - if constrained is None: - constrained = mpl.rcParams['figure.constrained_layout.use'] - self._constrained = bool(constrained) - if isinstance(constrained, dict): - self.set_constrained_layout_pads(**constrained) - else: - self.set_constrained_layout_pads() - - self.init_layoutgrid() - - self.stale = True - - def set_constrained_layout_pads(self, **kwargs): - """ - Set padding for ``constrained_layout``. Note the kwargs can be passed - as a dictionary ``fig.set_constrained_layout(**paddict)``. - - See :doc:`/tutorials/intermediate/constrainedlayout_guide`. - - Parameters - ---------- - w_pad : float - Width padding in inches. This is the pad around axes - and is meant to make sure there is enough room for fonts to - look good. Defaults to 3 pts = 0.04167 inches - - h_pad : float - Height padding in inches. Defaults to 3 pts. - - wspace : float - Width padding between subplots, expressed as a fraction of the - subplot width. The total padding ends up being w_pad + wspace. - - hspace : float - Height padding between subplots, expressed as a fraction of the - subplot width. The total padding ends up being h_pad + hspace. - - """ - - todo = ['w_pad', 'h_pad', 'wspace', 'hspace'] - for td in todo: - if td in kwargs and kwargs[td] is not None: - self._constrained_layout_pads[td] = kwargs[td] - else: - self._constrained_layout_pads[td] = ( - mpl.rcParams['figure.constrained_layout.' + td]) - - def get_constrained_layout_pads(self, relative=False): - """ - Get padding for ``constrained_layout``. - - Returns a list of ``w_pad, h_pad`` in inches and - ``wspace`` and ``hspace`` as fractions of the subplot. - - See :doc:`/tutorials/intermediate/constrainedlayout_guide`. - - Parameters - ---------- - relative : bool - If `True`, then convert from inches to figure relative. - """ - w_pad = self._constrained_layout_pads['w_pad'] - h_pad = self._constrained_layout_pads['h_pad'] - wspace = self._constrained_layout_pads['wspace'] - hspace = self._constrained_layout_pads['hspace'] - - if relative and (w_pad is not None or h_pad is not None): - renderer0 = layoutgrid.get_renderer(self) - dpi = renderer0.dpi - w_pad = w_pad * dpi / renderer0.width - h_pad = h_pad * dpi / renderer0.height - - return w_pad, h_pad, wspace, hspace - - def autofmt_xdate( - self, bottom=0.2, rotation=30, ha='right', which='major'): - """ - Date ticklabels often overlap, so it is useful to rotate them - and right align them. Also, a common use case is a number of - subplots with shared xaxes where the x-axis is date data. The - ticklabels are often long, and it helps to rotate them on the - bottom subplot and turn them off on other subplots, as well as - turn off xlabels. - - Parameters - ---------- - bottom : float, default: 0.2 - The bottom of the subplots for `subplots_adjust`. - rotation : float, default: 30 degrees - The rotation angle of the xtick labels in degrees. - ha : {'left', 'center', 'right'}, default: 'right' - The horizontal alignment of the xticklabels. - which : {'major', 'minor', 'both'}, default: 'major' - Selects which ticklabels to rotate. - """ - if which is None: - cbook.warn_deprecated( - "3.3", message="Support for passing which=None to mean " - "which='major' is deprecated since %(since)s and will be " - "removed %(removal)s.") - allsubplots = all(hasattr(ax, 'get_subplotspec') for ax in self.axes) - if len(self.axes) == 1: - for label in self.axes[0].get_xticklabels(which=which): - label.set_ha(ha) - label.set_rotation(rotation) - else: - if allsubplots: - for ax in self.get_axes(): - if ax.get_subplotspec().is_last_row(): - for label in ax.get_xticklabels(which=which): - label.set_ha(ha) - label.set_rotation(rotation) - else: - for label in ax.get_xticklabels(which=which): - label.set_visible(False) - ax.set_xlabel('') - - if allsubplots: - self.subplots_adjust(bottom=bottom) - self.stale = True - - def get_children(self): - """Get a list of artists contained in the figure.""" - return [self.patch, - *self.artists, - *self.axes, - *self.lines, - *self.patches, - *self.texts, - *self.images, - *self.legends] - - def contains(self, mouseevent): - """ - Test whether the mouse event occurred on the figure. + Test whether the mouse event occurred on the figure. Returns ------- @@ -733,213 +443,58 @@ def suptitle(self, t, **kwargs): sup.remove() else: self._suptitle = sup + if manual_position: self._suptitle.set_in_layout(False) + self.stale = True return self._suptitle - def set_canvas(self, canvas): + def get_edgecolor(self): + """Get the edge color of the Figure rectangle.""" + return self.patch.get_edgecolor() + + def get_facecolor(self): + """Get the face color of the Figure rectangle.""" + return self.patch.get_facecolor() + + def get_frameon(self): """ - Set the canvas that contains the figure + Return the figure's background patch visibility, i.e. + whether the figure background will be drawn. Equivalent to + ``Figure.patch.get_visible()``. + """ + return self.patch.get_visible() + + def set_linewidth(self, linewidth): + """ + Set the line width of the Figure rectangle. Parameters ---------- - canvas : FigureCanvas + linewidth : number """ - self.canvas = canvas + self.patch.set_linewidth(linewidth) - def figimage(self, X, xo=0, yo=0, alpha=None, norm=None, cmap=None, - vmin=None, vmax=None, origin=None, resize=False, **kwargs): + def get_linewidth(self): """ - Add a non-resampled image to the figure. + Get the line width of the Figure rectangle. + """ + return self.patch.get_linewidth() - The image is attached to the lower or upper left corner depending on - *origin*. + def set_edgecolor(self, color): + """ + Set the edge color of the Figure rectangle. Parameters ---------- - X - The image data. This is an array of one of the following shapes: + color : color + """ + self.patch.set_edgecolor(color) - - MxN: luminance (grayscale) values - - MxNx3: RGB values - - MxNx4: RGBA values - - xo, yo : int - The *x*/*y* image offset in pixels. - - alpha : None or float - The alpha blending value. - - norm : `matplotlib.colors.Normalize` - A `.Normalize` instance to map the luminance to the - interval [0, 1]. - - cmap : str or `matplotlib.colors.Colormap`, default: :rc:`image.cmap` - The colormap to use. - - vmin, vmax : float - If *norm* is not given, these values set the data limits for the - colormap. - - origin : {'upper', 'lower'}, default: :rc:`image.origin` - Indicates where the [0, 0] index of the array is in the upper left - or lower left corner of the axes. - - resize : bool - If *True*, resize the figure to match the given image size. - - Returns - ------- - `matplotlib.image.FigureImage` - - Other Parameters - ---------------- - **kwargs - Additional kwargs are `.Artist` kwargs passed on to `.FigureImage`. - - Notes - ----- - figimage complements the axes image (`~matplotlib.axes.Axes.imshow`) - which will be resampled to fit the current axes. If you want - a resampled image to fill the entire figure, you can define an - `~matplotlib.axes.Axes` with extent [0, 0, 1, 1]. - - Examples - -------- - :: - - f = plt.figure() - nx = int(f.get_figwidth() * f.dpi) - ny = int(f.get_figheight() * f.dpi) - data = np.random.random((ny, nx)) - f.figimage(data) - plt.show() - """ - if resize: - dpi = self.get_dpi() - figsize = [x / dpi for x in (X.shape[1], X.shape[0])] - self.set_size_inches(figsize, forward=True) - - im = mimage.FigureImage(self, cmap, norm, xo, yo, origin, **kwargs) - im.stale_callback = _stale_figure_callback - - im.set_array(X) - im.set_alpha(alpha) - if norm is None: - im.set_clim(vmin, vmax) - self.images.append(im) - im._remove_method = self.images.remove - self.stale = True - return im - - def set_size_inches(self, w, h=None, forward=True): - """ - Set the figure size in inches. - - Call signatures:: - - fig.set_size_inches(w, h) # OR - fig.set_size_inches((w, h)) - - Parameters - ---------- - w : (float, float) or float - Width and height in inches (if height not specified as a separate - argument) or width. - h : float - Height in inches. - forward : bool, default: True - If ``True``, the canvas size is automatically updated, e.g., - you can resize the figure window from the shell. - - See Also - -------- - matplotlib.figure.Figure.get_size_inches - matplotlib.figure.Figure.set_figwidth - matplotlib.figure.Figure.set_figheight - - Notes - ----- - To transform from pixels to inches divide by `Figure.dpi`. - """ - if h is None: # Got called with a single pair as argument. - w, h = w - size = np.array([w, h]) - if not np.isfinite(size).all() or (size < 0).any(): - raise ValueError(f'figure size must be positive finite not {size}') - self.bbox_inches.p1 = size - if forward: - canvas = getattr(self, 'canvas') - if canvas is not None: - dpi_ratio = getattr(canvas, '_dpi_ratio', 1) - manager = getattr(canvas, 'manager', None) - if manager is not None: - manager.resize(*(size * self.dpi / dpi_ratio).astype(int)) - self.stale = True - - def get_size_inches(self): - """ - Return the current size of the figure in inches. - - Returns - ------- - ndarray - The size (width, height) of the figure in inches. - - See Also - -------- - matplotlib.figure.Figure.set_size_inches - matplotlib.figure.Figure.get_figwidth - matplotlib.figure.Figure.get_figheight - - Notes - ----- - The size in pixels can be obtained by multiplying with `Figure.dpi`. - """ - return np.array(self.bbox_inches.p1) - - def get_edgecolor(self): - """Get the edge color of the Figure rectangle.""" - return self.patch.get_edgecolor() - - def get_facecolor(self): - """Get the face color of the Figure rectangle.""" - return self.patch.get_facecolor() - - def get_figwidth(self): - """Return the figure width in inches.""" - return self.bbox_inches.width - - def get_figheight(self): - """Return the figure height in inches.""" - return self.bbox_inches.height - - def get_dpi(self): - """Return the resolution in dots per inch as a float.""" - return self.dpi - - def get_frameon(self): - """ - Return the figure's background patch visibility, i.e. - whether the figure background will be drawn. Equivalent to - ``Figure.patch.get_visible()``. - """ - return self.patch.get_visible() - - def set_edgecolor(self, color): - """ - Set the edge color of the Figure rectangle. - - Parameters - ---------- - color : color - """ - self.patch.set_edgecolor(color) - - def set_facecolor(self, color): - """ - Set the face color of the Figure rectangle. + def set_facecolor(self, color): + """ + Set the face color of the Figure rectangle. Parameters ---------- @@ -947,51 +502,6 @@ def set_facecolor(self, color): """ self.patch.set_facecolor(color) - def set_dpi(self, val): - """ - Set the resolution of the figure in dots-per-inch. - - Parameters - ---------- - val : float - """ - self.dpi = val - self.stale = True - - def set_figwidth(self, val, forward=True): - """ - Set the width of the figure in inches. - - Parameters - ---------- - val : float - forward : bool - See `set_size_inches`. - - See Also - -------- - matplotlib.figure.Figure.set_figheight - matplotlib.figure.Figure.set_size_inches - """ - self.set_size_inches(val, self.get_figheight(), forward=forward) - - def set_figheight(self, val, forward=True): - """ - Set the height of the figure in inches. - - Parameters - ---------- - val : float - forward : bool - See `set_size_inches`. - - See Also - -------- - matplotlib.figure.Figure.set_figwidth - matplotlib.figure.Figure.set_size_inches - """ - self.set_size_inches(self.get_figwidth(), val, forward=forward) - def set_frameon(self, b): """ Set the figure's background patch visibility, i.e. @@ -1020,7 +530,7 @@ def add_artist(self, artist, clip=False): artist : `~matplotlib.artist.Artist` The artist to add to the figure. If the added artist has no transform previously set, its transform will be set to - ``figure.transFigure``. + ``figure.transSubfigure``. clip : bool, default: False Whether the added artist should be clipped by the figure patch. @@ -1034,7 +544,7 @@ def add_artist(self, artist, clip=False): artist._remove_method = self.artists.remove if not artist.is_transform_set(): - artist.set_transform(self.transFigure) + artist.set_transform(self.transSubfigure) if clip: artist.set_clip_path(self.patch) @@ -1042,86 +552,15 @@ def add_artist(self, artist, clip=False): self.stale = True return artist - def _make_key(self, *args, **kwargs): - """Make a hashable key out of args and kwargs.""" - - def fixitems(items): - # items may have arrays and lists in them, so convert them - # to tuples for the key - ret = [] - for k, v in items: - # some objects can define __getitem__ without being - # iterable and in those cases the conversion to tuples - # will fail. So instead of using the np.iterable(v) function - # we simply try and convert to a tuple, and proceed if not. - try: - v = tuple(v) - except Exception: - pass - ret.append((k, v)) - return tuple(ret) + @docstring.dedent_interpd + def add_axes(self, *args, **kwargs): + """ + Add an axes to the figure. - def fixlist(args): - ret = [] - for a in args: - if np.iterable(a): - a = tuple(a) - ret.append(a) - return tuple(ret) + Call signatures:: - key = fixlist(args), fixitems(kwargs.items()) - return key - - def _process_projection_requirements( - self, *args, axes_class=None, polar=False, projection=None, - **kwargs): - """ - Handle the args/kwargs to add_axes/add_subplot/gca, returning:: - - (axes_proj_class, proj_class_kwargs, proj_stack_key) - - which can be used for new axes initialization/identification. - """ - if axes_class is not None: - if polar or projection is not None: - raise ValueError( - "Cannot combine 'axes_class' and 'projection' or 'polar'") - projection_class = axes_class - else: - - if polar: - if projection is not None and projection != 'polar': - raise ValueError( - "polar=True, yet projection=%r. " - "Only one of these arguments should be supplied." % - projection) - projection = 'polar' - - if isinstance(projection, str) or projection is None: - projection_class = projections.get_projection_class(projection) - elif hasattr(projection, '_as_mpl_axes'): - projection_class, extra_kwargs = projection._as_mpl_axes() - kwargs.update(**extra_kwargs) - else: - raise TypeError( - f"projection must be a string, None or implement a " - f"_as_mpl_axes method, not {projection!r}") - - # Make the key without projection kwargs, this is used as a unique - # lookup for axes instances - key = self._make_key(*args, **kwargs) - - return projection_class, kwargs, key - - @docstring.dedent_interpd - def add_axes(self, *args, **kwargs): - """ - Add an axes to the figure. - - Call signatures:: - - add_axes(rect, projection=None, polar=False, **kwargs) - add_axes(ax) + add_axes(rect, projection=None, polar=False, **kwargs) + add_axes(ax) Parameters ---------- @@ -1251,7 +690,6 @@ def add_axes(self, *args, **kwargs): # create the new axes using the axes class given a = projection_class(self, rect, **kwargs) - return self._add_axes_internal(key, a) @docstring.dedent_interpd @@ -1408,13 +846,12 @@ def add_subplot(self, *args, **kwargs): # more similar to add_axes. self._axstack.remove(ax) ax = subplot_class_factory(projection_class)(self, *args, **kwargs) - return self._add_axes_internal(key, ax) def _add_axes_internal(self, key, ax): """Private helper for `add_axes` and `add_subplot`.""" - #self._localaxes += [ax] self._axstack.add(key, ax) + self._localaxes.add(key, ax) self.sca(ax) ax._remove_method = self.delaxes self.stale = True @@ -1528,209 +965,10 @@ def subplots(self, nrows=1, ncols=1, sharex=False, sharey=False, """ if gridspec_kw is None: gridspec_kw = {} - return (self.add_gridspec(nrows, ncols, figure=self, **gridspec_kw) - .subplots(sharex=sharex, sharey=sharey, squeeze=squeeze, - subplot_kw=subplot_kw)) - - @staticmethod - def _normalize_grid_string(layout): - layout = inspect.cleandoc(layout) - return [list(ln) for ln in layout.strip('\n').split('\n')] - - def subplot_mosaic(self, layout, *, subplot_kw=None, gridspec_kw=None, - empty_sentinel='.'): - """ - Build a layout of Axes based on ASCII art or nested lists. - - This is a helper function to build complex GridSpec layouts visually. - - .. note :: - - This API is provisional and may be revised in the future based on - early user feedback. - - - Parameters - ---------- - layout : list of list of {hashable or nested} or str - - A visual layout of how you want your Axes to be arranged - labeled as strings. For example :: - - x = [['A panel', 'A panel', 'edge'], - ['C panel', '.', 'edge']] - - Produces 4 axes: - - - 'A panel' which is 1 row high and spans the first two columns - - 'edge' which is 2 rows high and is on the right edge - - 'C panel' which in 1 row and 1 column wide in the bottom left - - a blank space 1 row and 1 column wide in the bottom center - - Any of the entries in the layout can be a list of lists - of the same form to create nested layouts. - - If input is a str, then it must be of the form :: - - ''' - AAE - C.E - ''' - - where each character is a column and each line is a row. - This only allows only single character Axes labels and does - not allow nesting but is very terse. - - 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. - - empty_sentinel : object, optional - Entry in the layout to mean "leave this space empty". Defaults - to ``'.'``. Note, if *layout* is a string, it is processed via - `inspect.cleandoc` to remove leading white space, which may - interfere with using white-space as the empty sentinel. - - Returns - ------- - dict[label, Axes] - A dictionary mapping the labels to the Axes objects. - - """ - subplot_kw = subplot_kw or {} - gridspec_kw = gridspec_kw or {} - # special-case string input - if isinstance(layout, str): - layout = self._normalize_grid_string(layout) - - def _make_array(inp): - """ - Convert input into 2D array - - We need to have this internal function rather than - ``np.asarray(..., dtype=object)`` so that a list of lists - of lists does not get converted to an array of dimension > - 2 - - Returns - ------- - 2D object array - - """ - r0, *rest = inp - for j, r in enumerate(rest, start=1): - if isinstance(r, str): - raise ValueError('List layout specification must be 2D') - if len(r0) != len(r): - raise ValueError( - "All of the rows must be the same length, however " - f"the first row ({r0!r}) has length {len(r0)} " - f"and row {j} ({r!r}) has length {len(r)}." - ) - out = np.zeros((len(inp), len(r0)), dtype=object) - for j, r in enumerate(inp): - for k, v in enumerate(r): - out[j, k] = v - return out - - def _identify_keys_and_nested(layout): - """ - Given a 2D object array, identify unique IDs and nested layouts - - Parameters - ---------- - layout : 2D numpy object array - - Returns - ------- - unique_ids : Set[object] - The unique non-sub layout entries in this layout - nested : Dict[Tuple[int, int]], 2D object array - """ - unique_ids = set() - nested = {} - for j, row in enumerate(layout): - for k, v in enumerate(row): - if v == empty_sentinel: - continue - elif not cbook.is_scalar_or_string(v): - nested[(j, k)] = _make_array(v) - else: - unique_ids.add(v) - - return unique_ids, nested - - def _do_layout(gs, layout, unique_ids, nested): - """ - Recursively do the layout. - - Parameters - ---------- - gs : GridSpec - - layout : 2D object array - The input converted to a 2D numpy array for this level. - - unique_ids : Set[object] - The identified scalar labels at this level of nesting. - - nested : Dict[Tuple[int, int]], 2D object array - The identified nested layouts if any. - - Returns - ------- - Dict[label, Axes] - A flat dict of all of the Axes created. - """ - rows, cols = layout.shape - output = dict() - - # create the Axes at this level of nesting - for name in unique_ids: - indx = np.argwhere(layout == name) - start_row, start_col = np.min(indx, axis=0) - end_row, end_col = np.max(indx, axis=0) + 1 - slc = (slice(start_row, end_row), slice(start_col, end_col)) - - if (layout[slc] != name).any(): - raise ValueError( - f"While trying to layout\n{layout!r}\n" - f"we found that the label {name!r} specifies a " - "non-rectangular or non-contiguous area.") - - ax = self.add_subplot( - gs[slc], **{'label': str(name), **subplot_kw} - ) - output[name] = ax - - # do any sub-layouts - for (j, k), nested_layout in nested.items(): - rows, cols = nested_layout.shape - nested_output = _do_layout( - gs[j, k].subgridspec(rows, cols, **gridspec_kw), - nested_layout, - *_identify_keys_and_nested(nested_layout) - ) - overlap = set(output) & set(nested_output) - if overlap: - raise ValueError(f"There are duplicate keys {overlap} " - f"between the outer layout\n{layout!r}\n" - f"and the nested layout\n{nested_layout}") - output.update(nested_output) - return output - - layout = _make_array(layout) - rows, cols = layout.shape - gs = self.add_gridspec(rows, cols, **gridspec_kw) - ret = _do_layout(gs, layout, *_identify_keys_and_nested(layout)) - for k, ax in ret.items(): - if isinstance(k, str): - ax.set_label(k) - return ret + gs = self.add_gridspec(nrows, ncols, figure=self, **gridspec_kw) + axs = gs.subplots(sharex=sharex, sharey=sharey, squeeze=squeeze, + subplot_kw=subplot_kw) + return axs def delaxes(self, ax): """ @@ -1779,9 +1017,9 @@ def _break_share_link(ax, grouper): return None self._axstack.remove(ax) - # self._localaxes.remove(ax) self._axobservers.process("_axes_change_event", self) self.stale = True + self._localaxes.remove(ax) last_ax = _break_share_link(ax, ax._shared_y_axes) if last_ax is not None: @@ -1791,106 +1029,6 @@ def _break_share_link(ax, grouper): if last_ax is not None: _reset_locators_and_formatters(last_ax.xaxis) - def clf(self, keep_observers=False): - """ - Clear the figure. - - Set *keep_observers* to True if, for example, - a gui widget is tracking the axes in the figure. - """ - self.suppressComposite = None - self.callbacks = cbook.CallbackRegistry() - - for ax in tuple(self.axes): # Iterate over the copy. - ax.cla() - self.delaxes(ax) # removes ax from self._axstack - - toolbar = getattr(self.canvas, 'toolbar', None) - if toolbar is not None: - toolbar.update() - self._axstack.clear() - self.artists = [] - self.lines = [] - self.patches = [] - self.texts = [] - self.images = [] - self.legends = [] - if not keep_observers: - self._axobservers = cbook.CallbackRegistry() - self._suptitle = None - if self.get_constrained_layout(): - self.init_layoutgrid() - self.stale = True - - def clear(self, keep_observers=False): - """Clear the figure -- synonym for `clf`.""" - self.clf(keep_observers=keep_observers) - - @_finalize_rasterization - @allow_rasterization - def draw(self, renderer): - # docstring inherited - self._cachedRenderer = renderer - - # draw the figure bounding box, perhaps none for white figure - if not self.get_visible(): - return - - artists = self.get_children() - artists.remove(self.patch) - artists = sorted( - (artist for artist in artists if not artist.get_animated()), - key=lambda artist: artist.get_zorder()) - - for ax in self.axes: - locator = ax.get_axes_locator() - if locator: - pos = locator(ax, renderer) - ax.apply_aspect(pos) - else: - ax.apply_aspect() - - for child in ax.get_children(): - if hasattr(child, 'apply_aspect'): - locator = child.get_axes_locator() - if locator: - pos = locator(child, renderer) - child.apply_aspect(pos) - else: - child.apply_aspect() - - try: - renderer.open_group('figure', gid=self.get_gid()) - if self.get_constrained_layout() and self.axes: - self.execute_constrained_layout(renderer) - if self.get_tight_layout() and self.axes: - try: - self.tight_layout(**self._tight_parameters) - except ValueError: - pass - # ValueError can occur when resizing a window. - - self.patch.draw(renderer) - mimage._draw_list_compositing_images( - renderer, self, artists, self.suppressComposite) - - renderer.close_group('figure') - finally: - self.stale = False - - self.canvas.draw_event(renderer) - - def draw_artist(self, a): - """ - Draw `.Artist` instance *a* only. - - This can only be called after the figure has been drawn. - """ - if self._cachedRenderer is None: - raise AttributeError("draw_artist can only be used after an " - "initial draw which caches the renderer") - a.draw(self._cachedRenderer) - # Note: in the docstring below, the newlines in the examples after the # calls to legend() allow replacing it with figlegend() to generate the # docstring of pyplot.figlegend. @@ -2007,7 +1145,7 @@ def legend(self, *args, **kwargs): # kwargs['loc'] = extra_args[0] # extra_args = extra_args[1:] pass - transform = kwargs.pop('bbox_transform', self.transFigure) + transform = kwargs.pop('bbox_transform', self.transSubfigure) # explicitly set the bbox transform if the user hasn't. l = mlegend.Legend(self, handles, labels, *extra_args, bbox_transform=transform, **kwargs) @@ -2040,123 +1178,1683 @@ def text(self, x, y, s, fontdict=None, **kwargs): ------- `~.text.Text` - Other Parameters - ---------------- - **kwargs : `~matplotlib.text.Text` properties - Other miscellaneous text parameters. + Other Parameters + ---------------- + **kwargs : `~matplotlib.text.Text` properties + Other miscellaneous text parameters. + + %(Text)s + + See Also + -------- + .Axes.text + .pyplot.text + """ + effective_kwargs = { + 'transform': self.transSubfigure, + **(fontdict if fontdict is not None else {}), + **kwargs, + } + text = Text(x=x, y=y, text=s, **effective_kwargs) + text.set_figure(self) + text.stale_callback = _stale_figure_callback + + self.texts.append(text) + text._remove_method = self.texts.remove + self.stale = True + return text + + @docstring.dedent_interpd + def colorbar(self, mappable, cax=None, ax=None, use_gridspec=True, **kw): + """%(colorbar_doc)s""" + if ax is None: + ax = self.gca() + if (hasattr(mappable, "axes") and ax is not mappable.axes + and cax is None): + cbook.warn_deprecated( + "3.4", message="Starting from Matplotlib 3.6, colorbar() " + "will steal space from the mappable's axes, rather than " + "from the current axes, to place the colorbar. To " + "silence this warning, explicitly pass the 'ax' argument " + "to colorbar().") + + # Store the value of gca so that we can set it back later on. + current_ax = self.gca() + if cax is None: + if (use_gridspec and isinstance(ax, SubplotBase) + and not self.get_constrained_layout()): + cax, kw = cbar.make_axes_gridspec(ax, **kw) + else: + cax, kw = cbar.make_axes(ax, **kw) + + # need to remove kws that cannot be passed to Colorbar + NON_COLORBAR_KEYS = ['fraction', 'pad', 'shrink', 'aspect', 'anchor', + 'panchor'] + cb_kw = {k: v for k, v in kw.items() if k not in NON_COLORBAR_KEYS} + cb = cbar.Colorbar(cax, mappable, **cb_kw) + + self.sca(current_ax) + self.stale = True + return cb + + def subplots_adjust(self, left=None, bottom=None, right=None, top=None, + wspace=None, hspace=None): + """ + Adjust the subplot layout parameters. + + Unset parameters are left unmodified; initial values are given by + :rc:`figure.subplot.[name]`. + + Parameters + ---------- + left : float, optional + The position of the left edge of the subplots, + as a fraction of the figure width. + right : float, optional + The position of the right edge of the subplots, + as a fraction of the figure width. + bottom : float, optional + The position of the bottom edge of the subplots, + as a fraction of the figure height. + top : float, optional + The position of the top edge of the subplots, + as a fraction of the figure height. + wspace : float, optional + The width of the padding between subplots, + as a fraction of the average axes width. + hspace : float, optional + The height of the padding between subplots, + as a fraction of the average axes height. + """ + if self.get_constrained_layout(): + self.set_constrained_layout(False) + cbook._warn_external( + "This figure was using constrained_layout, but that is " + "incompatible with subplots_adjust and/or tight_layout; " + "disabling constrained_layout.") + self.subplotpars.update(left, bottom, right, top, wspace, hspace) + for ax in self.axes: + if isinstance(ax, SubplotBase): + ax._set_position(ax.get_subplotspec().get_position(self)) + self.stale = True + + def align_xlabels(self, axs=None): + """ + Align the xlabels of subplots in the same subplot column if label + alignment is being done automatically (i.e. the label position is + not manually set). + + Alignment persists for draw events after this is called. + + If a label is on the bottom, it is aligned with labels on axes that + also have their label on the bottom and that have the same + bottom-most subplot row. If the label is on the top, + it is aligned with labels on axes with the same top-most row. + + Parameters + ---------- + axs : list of `~matplotlib.axes.Axes` + Optional list of (or ndarray) `~matplotlib.axes.Axes` + to align the xlabels. + Default is to align all axes on the figure. + + See Also + -------- + matplotlib.figure.Figure.align_ylabels + matplotlib.figure.Figure.align_labels + + Notes + ----- + This assumes that ``axs`` are from the same `.GridSpec`, so that + their `.SubplotSpec` positions correspond to figure positions. + + Examples + -------- + Example with rotated xtick labels:: + + fig, axs = plt.subplots(1, 2) + for tick in axs[0].get_xticklabels(): + tick.set_rotation(55) + axs[0].set_xlabel('XLabel 0') + axs[1].set_xlabel('XLabel 1') + fig.align_xlabels() + """ + if axs is None: + axs = self.axes + axs = np.ravel(axs) + for ax in axs: + _log.debug(' Working on: %s', ax.get_xlabel()) + rowspan = ax.get_subplotspec().rowspan + pos = ax.xaxis.get_label_position() # top or bottom + # Search through other axes for label positions that are same as + # this one and that share the appropriate row number. + # Add to a grouper associated with each axes of siblings. + # This list is inspected in `axis.draw` by + # `axis._update_label_position`. + for axc in axs: + if axc.xaxis.get_label_position() == pos: + rowspanc = axc.get_subplotspec().rowspan + if (pos == 'top' and rowspan.start == rowspanc.start or + pos == 'bottom' and rowspan.stop == rowspanc.stop): + # grouper for groups of xlabels to align + self._align_xlabel_grp.join(ax, axc) + + def align_ylabels(self, axs=None): + """ + Align the ylabels of subplots in the same subplot column if label + alignment is being done automatically (i.e. the label position is + not manually set). + + Alignment persists for draw events after this is called. + + If a label is on the left, it is aligned with labels on axes that + also have their label on the left and that have the same + left-most subplot column. If the label is on the right, + it is aligned with labels on axes with the same right-most column. + + Parameters + ---------- + axs : list of `~matplotlib.axes.Axes` + Optional list (or ndarray) of `~matplotlib.axes.Axes` + to align the ylabels. + Default is to align all axes on the figure. + + See Also + -------- + matplotlib.figure.Figure.align_xlabels + matplotlib.figure.Figure.align_labels + + Notes + ----- + This assumes that ``axs`` are from the same `.GridSpec`, so that + their `.SubplotSpec` positions correspond to figure positions. + + Examples + -------- + Example with large yticks labels:: + + fig, axs = plt.subplots(2, 1) + axs[0].plot(np.arange(0, 1000, 50)) + axs[0].set_ylabel('YLabel 0') + axs[1].set_ylabel('YLabel 1') + fig.align_ylabels() + """ + if axs is None: + axs = self.axes + axs = np.ravel(axs) + for ax in axs: + _log.debug(' Working on: %s', ax.get_ylabel()) + colspan = ax.get_subplotspec().colspan + pos = ax.yaxis.get_label_position() # left or right + # Search through other axes for label positions that are same as + # this one and that share the appropriate column number. + # Add to a list associated with each axes of siblings. + # This list is inspected in `axis.draw` by + # `axis._update_label_position`. + for axc in axs: + if axc.yaxis.get_label_position() == pos: + colspanc = axc.get_subplotspec().colspan + if (pos == 'left' and colspan.start == colspanc.start or + pos == 'right' and colspan.stop == colspanc.stop): + # grouper for groups of ylabels to align + self._align_ylabel_grp.join(ax, axc) + + def align_labels(self, axs=None): + """ + Align the xlabels and ylabels of subplots with the same subplots + row or column (respectively) if label alignment is being + done automatically (i.e. the label position is not manually set). + + Alignment persists for draw events after this is called. + + Parameters + ---------- + axs : list of `~matplotlib.axes.Axes` + Optional list (or ndarray) of `~matplotlib.axes.Axes` + to align the labels. + Default is to align all axes on the figure. + + See Also + -------- + matplotlib.figure.Figure.align_xlabels + + matplotlib.figure.Figure.align_ylabels + """ + self.align_xlabels(axs=axs) + self.align_ylabels(axs=axs) + + def add_gridspec(self, nrows=1, ncols=1, **kwargs): + """ + Return a `.GridSpec` that has this figure as a parent. This allows + complex layout of axes in the figure. + + Parameters + ---------- + nrows : int, default: 1 + Number of rows in grid. + + ncols : int, default: 1 + Number or columns in grid. + + Returns + ------- + `.GridSpec` + + Other Parameters + ---------------- + **kwargs + Keyword arguments are passed to `.GridSpec`. + + See Also + -------- + matplotlib.pyplot.subplots + + Examples + -------- + Adding a subplot that spans two rows:: + + fig = plt.figure() + gs = fig.add_gridspec(2, 2) + ax1 = fig.add_subplot(gs[0, 0]) + ax2 = fig.add_subplot(gs[1, 0]) + # spans two rows: + ax3 = fig.add_subplot(gs[:, 1]) + + """ + + _ = kwargs.pop('figure', None) # pop in case user has added this... + gs = GridSpec(nrows=nrows, ncols=ncols, figure=self, **kwargs) + self._gridspecs.append(gs) + return gs + + def subfigures(self, nrows=1, ncols=1, squeeze=True, + wspace=None, hspace=None, + width_ratios=None, height_ratios=None, + **kwargs): + """ + Add a subfigure to this figure or subfigure. + + A subfigure has the same artist methods as a figure, and is logically + the same as a figure, but cannot print itself. + See :doc:`/gallery/subplots_axes_and_figures/subfigures`. + + Parameters + ---------- + nrows, ncols : int, default: 1 + Number of rows/columns of the subfigure grid. + + squeeze : bool, default: True + If True, extra dimensions are squeezed out from the returned + array of subfigures. + + wspace, hspace : float, default: None + The amount of width/height reserved for space between subfigures, + expressed as a fraction of the average subfigure width/height. + If not given, the values will be inferred from a figure or + rcParams when necessary. + + 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. + + 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. + """ + gs = GridSpec(nrows=nrows, ncols=ncols, figure=self, + wspace=wspace, hspace=hspace, + width_ratios=width_ratios, + height_ratios=height_ratios) + + sfarr = np.empty((nrows, ncols), dtype=object) + for i in range(ncols): + for j in range(nrows): + sfarr[j, i] = self.add_subfigure(gs[j, i], **kwargs) + + if squeeze: + # Discarding unneeded dimensions that equal 1. If we only have one + # subfigure, just return it instead of a 1-element array. + return sfarr.item() if sfarr.size == 1 else sfarr.squeeze() + else: + # Returned axis array will be always 2-d, even if nrows=ncols=1. + return sfarr + + return sfarr + + def add_subfigure(self, subplotspec, **kwargs): + """ + Add a `~.figure.SubFigure` to the figure as part of a subplot + arrangement. + + Parameters + ---------- + subplotspec : `.gridspec.SubplotSpec` + Defines the region in a parent gridspec where the subfigure will + be placed. + + Returns + ------- + `.figure.SubFigure` + + Other Parameters + ---------------- + **kwargs + Are passed to the `~.figure.SubFigure` object. + + See Also + -------- + .Figure.subfigures + """ + sf = SubFigure(self, subplotspec, **kwargs) + self.subfigs += [sf] + return sf + + def sca(self, a): + """Set the current axes to be *a* and return *a*.""" + self._axstack.bubble(a) + self._axobservers.process("_axes_change_event", self) + return a + + @docstring.dedent_interpd + def gca(self, **kwargs): + """ + Get the current axes, creating one if necessary. + + The following kwargs are supported for ensuring the returned axes + adheres to the given projection etc., and for axes creation if + the active axes does not exist: + + %(Axes)s + + """ + ckey, cax = self._axstack.current_key_axes() + # if there exists an axes on the stack see if it matches + # the desired axes configuration + if cax is not None: + + # if no kwargs are given just return the current axes + # this is a convenience for gca() on axes such as polar etc. + if not kwargs: + return cax + + # if the user has specified particular projection detail + # then build up a key which can represent this + else: + projection_class, _, key = \ + self._process_projection_requirements(**kwargs) + + # let the returned axes have any gridspec by removing it from + # the key + ckey = ckey[1:] + key = key[1:] + + # if the cax matches this key then return the axes, otherwise + # continue and a new axes will be created + if key == ckey and isinstance(cax, projection_class): + return cax + else: + cbook._warn_external('Requested projection is different ' + 'from current axis projection, ' + 'creating new axis with requested ' + 'projection.') + + # no axes found, so create one which spans the figure + return self.add_subplot(1, 1, 1, **kwargs) + + def _gci(self): + # Helper for `~matplotlib.pyplot.gci`. Do not use elsewhere. + """ + Get the current colorable artist. + + Specifically, returns the current `.ScalarMappable` instance (`.Image` + created by `imshow` or `figimage`, `.Collection` created by `pcolor` or + `scatter`, etc.), or *None* if no such instance has been defined. + + The current image is an attribute of the current axes, or the nearest + earlier axes in the current figure that contains an image. + + Notes + ----- + Historically, the only colorable artists were images; hence the name + ``gci`` (get current image). + """ + # Look first for an image in the current Axes: + cax = self._axstack.current_key_axes()[1] + if cax is None: + return None + im = cax._gci() + if im is not None: + return im + + # If there is no image in the current Axes, search for + # one in a previously created Axes. Whether this makes + # sense is debatable, but it is the documented behavior. + for ax in reversed(self.axes): + im = ax._gci() + if im is not None: + return im + return None + + def _process_projection_requirements( + self, *args, axes_class=None, polar=False, projection=None, + **kwargs): + """ + Handle the args/kwargs to add_axes/add_subplot/gca, returning:: + + (axes_proj_class, proj_class_kwargs, proj_stack_key) + + which can be used for new axes initialization/identification. + """ + if axes_class is not None: + if polar or projection is not None: + raise ValueError( + "Cannot combine 'axes_class' and 'projection' or 'polar'") + projection_class = axes_class + else: + + if polar: + if projection is not None and projection != 'polar': + raise ValueError( + "polar=True, yet projection=%r. " + "Only one of these arguments should be supplied." % + projection) + projection = 'polar' + + if isinstance(projection, str) or projection is None: + projection_class = projections.get_projection_class(projection) + elif hasattr(projection, '_as_mpl_axes'): + projection_class, extra_kwargs = projection._as_mpl_axes() + kwargs.update(**extra_kwargs) + else: + raise TypeError( + f"projection must be a string, None or implement a " + f"_as_mpl_axes method, not {projection!r}") + + # Make the key without projection kwargs, this is used as a unique + # lookup for axes instances + key = self._make_key(*args, **kwargs) + + return projection_class, kwargs, key + + def _make_key(self, *args, **kwargs): + """Make a hashable key out of args and kwargs.""" + + def fixitems(items): + # items may have arrays and lists in them, so convert them + # to tuples for the key + ret = [] + for k, v in items: + # some objects can define __getitem__ without being + # iterable and in those cases the conversion to tuples + # will fail. So instead of using the np.iterable(v) function + # we simply try and convert to a tuple, and proceed if not. + try: + v = tuple(v) + except Exception: + pass + ret.append((k, v)) + return tuple(ret) + + def fixlist(args): + ret = [] + for a in args: + if np.iterable(a): + a = tuple(a) + ret.append(a) + return tuple(ret) + + key = fixlist(args), fixitems(kwargs.items()) + return key + + def get_default_bbox_extra_artists(self): + bbox_artists = [artist for artist in self.get_children() + if (artist.get_visible() and artist.get_in_layout())] + for ax in self.axes: + if ax.get_visible(): + bbox_artists.extend(ax.get_default_bbox_extra_artists()) + return bbox_artists + + def get_tightbbox(self, renderer, bbox_extra_artists=None): + """ + Return a (tight) bounding box of the figure in inches. + + Artists that have ``artist.set_in_layout(False)`` are not included + in the bbox. + + Parameters + ---------- + renderer : `.RendererBase` subclass + renderer that will be used to draw the figures (i.e. + ``fig.canvas.get_renderer()``) + + bbox_extra_artists : list of `.Artist` or ``None`` + List of artists to include in the tight bounding box. If + ``None`` (default), then all artist children of each axes are + included in the tight bounding box. + + Returns + ------- + `.BboxBase` + containing the bounding box (in figure inches). + """ + + bb = [] + if bbox_extra_artists is None: + artists = self.get_default_bbox_extra_artists() + else: + artists = bbox_extra_artists + + for a in artists: + bbox = a.get_tightbbox(renderer) + if bbox is not None and (bbox.width != 0 or bbox.height != 0): + bb.append(bbox) + + for ax in self.axes: + if ax.get_visible(): + # some axes don't take the bbox_extra_artists kwarg so we + # need this conditional.... + try: + bbox = ax.get_tightbbox( + renderer, bbox_extra_artists=bbox_extra_artists) + except TypeError: + bbox = ax.get_tightbbox(renderer) + bb.append(bbox) + bb = [b for b in bb + if (np.isfinite(b.width) and np.isfinite(b.height) + and (b.width != 0 or b.height != 0))] + + if len(bb) == 0: + return self.bbox_inches + + _bbox = Bbox.union(bb) + + bbox_inches = TransformedBbox(_bbox, Affine2D().scale(1 / self.dpi)) + + return bbox_inches + + @staticmethod + def _normalize_grid_string(layout): + layout = inspect.cleandoc(layout) + return [list(ln) for ln in layout.strip('\n').split('\n')] + + def subplot_mosaic(self, layout, *, subplot_kw=None, gridspec_kw=None, + empty_sentinel='.'): + """ + Build a layout of Axes based on ASCII art or nested lists. + + This is a helper function to build complex GridSpec layouts visually. + + .. note :: + + This API is provisional and may be revised in the future based on + early user feedback. + + + Parameters + ---------- + layout : list of list of {hashable or nested} or str + + A visual layout of how you want your Axes to be arranged + labeled as strings. For example :: + + x = [['A panel', 'A panel', 'edge'], + ['C panel', '.', 'edge']] + + Produces 4 axes: + + - 'A panel' which is 1 row high and spans the first two columns + - 'edge' which is 2 rows high and is on the right edge + - 'C panel' which in 1 row and 1 column wide in the bottom left + - a blank space 1 row and 1 column wide in the bottom center + + Any of the entries in the layout can be a list of lists + of the same form to create nested layouts. + + If input is a str, then it must be of the form :: + + ''' + AAE + C.E + ''' + + where each character is a column and each line is a row. + This only allows only single character Axes labels and does + not allow nesting but is very terse. + + 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. + + empty_sentinel : object, optional + Entry in the layout to mean "leave this space empty". Defaults + to ``'.'``. Note, if *layout* is a string, it is processed via + `inspect.cleandoc` to remove leading white space, which may + interfere with using white-space as the empty sentinel. + + Returns + ------- + dict[label, Axes] + A dictionary mapping the labels to the Axes objects. + + """ + subplot_kw = subplot_kw or {} + gridspec_kw = gridspec_kw or {} + # special-case string input + if isinstance(layout, str): + layout = self._normalize_grid_string(layout) + + def _make_array(inp): + """ + Convert input into 2D array + + We need to have this internal function rather than + ``np.asarray(..., dtype=object)`` so that a list of lists + of lists does not get converted to an array of dimension > + 2 + + Returns + ------- + 2D object array + + """ + r0, *rest = inp + for j, r in enumerate(rest, start=1): + if isinstance(r, str): + raise ValueError('List layout specification must be 2D') + if len(r0) != len(r): + raise ValueError( + "All of the rows must be the same length, however " + f"the first row ({r0!r}) has length {len(r0)} " + f"and row {j} ({r!r}) has length {len(r)}." + ) + out = np.zeros((len(inp), len(r0)), dtype=object) + for j, r in enumerate(inp): + for k, v in enumerate(r): + out[j, k] = v + return out + + def _identify_keys_and_nested(layout): + """ + Given a 2D object array, identify unique IDs and nested layouts + + Parameters + ---------- + layout : 2D numpy object array + + Returns + ------- + unique_ids : Set[object] + The unique non-sub layout entries in this layout + nested : Dict[Tuple[int, int]], 2D object array + """ + unique_ids = set() + nested = {} + for j, row in enumerate(layout): + for k, v in enumerate(row): + if v == empty_sentinel: + continue + elif not cbook.is_scalar_or_string(v): + nested[(j, k)] = _make_array(v) + else: + unique_ids.add(v) + + return unique_ids, nested + + def _do_layout(gs, layout, unique_ids, nested): + """ + Recursively do the layout. + + Parameters + ---------- + gs : GridSpec + + layout : 2D object array + The input converted to a 2D numpy array for this level. + + unique_ids : Set[object] + The identified scalar labels at this level of nesting. + + nested : Dict[Tuple[int, int]], 2D object array + The identified nested layouts if any. + + Returns + ------- + Dict[label, Axes] + A flat dict of all of the Axes created. + """ + rows, cols = layout.shape + output = dict() + + # create the Axes at this level of nesting + for name in unique_ids: + indx = np.argwhere(layout == name) + start_row, start_col = np.min(indx, axis=0) + end_row, end_col = np.max(indx, axis=0) + 1 + slc = (slice(start_row, end_row), slice(start_col, end_col)) + + if (layout[slc] != name).any(): + raise ValueError( + f"While trying to layout\n{layout!r}\n" + f"we found that the label {name!r} specifies a " + "non-rectangular or non-contiguous area.") + + ax = self.add_subplot( + gs[slc], **{'label': str(name), **subplot_kw} + ) + output[name] = ax + + # do any sub-layouts + for (j, k), nested_layout in nested.items(): + rows, cols = nested_layout.shape + nested_output = _do_layout( + gs[j, k].subgridspec(rows, cols, **gridspec_kw), + nested_layout, + *_identify_keys_and_nested(nested_layout) + ) + overlap = set(output) & set(nested_output) + if overlap: + raise ValueError(f"There are duplicate keys {overlap} " + f"between the outer layout\n{layout!r}\n" + f"and the nested layout\n{nested_layout}") + output.update(nested_output) + return output + + layout = _make_array(layout) + rows, cols = layout.shape + gs = self.add_gridspec(rows, cols, **gridspec_kw) + ret = _do_layout(gs, layout, *_identify_keys_and_nested(layout)) + for k, ax in ret.items(): + if isinstance(k, str): + ax.set_label(k) + return ret + + def _set_artist_props(self, a): + if a != self: + a.set_figure(self) + a.stale_callback = _stale_figure_callback + a.set_transform(self.transSubfigure) + + +class SubFigure(FigureBase): + """ + Logical figure that can be placed inside a figure. + + Typically instantiated using `.Figure.add_subfigure` or + `.SubFigure.add_subfigure`, or `.SubFigure.subfigures`. A subfigure has + the same methods as a figure except for those particularly tied to the size + or dpi of the figure, and is confined to a prescribed region of the figure. + For example the following puts two subfigures side-by-side:: + + fig = plt.figure() + sfigs = fig.subfigures(1, 2) + axsL = sfigs[0].subplots(1, 2) + axsR = sfigs[1].subplots(2, 1) + + See :doc:`/gallery/subplots_axes_and_figures/subfigures` + """ + + def __init__(self, parent, subplotspec, *, + facecolor=None, + edgecolor=None, + linewidth=0.0, + frameon=None): + """ + Parameters + ---------- + parent : `.figure.Figure` or `.figure.SubFigure` + Figure or subfigure that contains the SubFigure. SubFigures + can be nested. + + subplotspec : `.gridspec.SubplotSpec` + Defines the region in a parent gridspec where the subfigure will + be placed. + + facecolor : default: :rc:`figure.facecolor` + The figure patch face color. + + edgecolor : default: :rc:`figure.edgecolor` + The figure patch edge color. + + linewidth : float + The linewidth of the frame (i.e. the edge linewidth of the figure + patch). + + frameon : bool, default: :rc:`figure.frameon` + If ``False``, suppress drawing the figure background patch. + """ + super().__init__() + if facecolor is None: + facecolor = mpl.rcParams['figure.facecolor'] + if edgecolor is None: + edgecolor = mpl.rcParams['figure.edgecolor'] + if frameon is None: + frameon = mpl.rcParams['figure.frameon'] + + self._subplotspec = subplotspec + self._parent = parent + self.figure = parent.figure + # subfigures use the parent axstack + self._axstack = parent._axstack + self.subplotpars = parent.subplotpars + self.dpi_scale_trans = parent.dpi_scale_trans + self._axobservers = parent._axobservers + self.dpi = parent.dpi + self.canvas = parent.canvas + self.transFigure = parent.transFigure + self.bbox_relative = None + self._redo_transform_rel_fig() + self.figbbox = self._parent.figbbox + self.bbox = TransformedBbox(self.bbox_relative, + self._parent.transSubfigure) + self.transSubfigure = BboxTransformTo(self.bbox) + + self.patch = Rectangle( + xy=(0, 0), width=1, height=1, visible=frameon, + facecolor=facecolor, edgecolor=edgecolor, linewidth=linewidth, + # Don't let the figure patch influence bbox calculation. + in_layout=False, transform=self.transSubfigure) + self._set_artist_props(self.patch) + self.patch.set_antialiased(False) + + if parent._layoutgrid is not None: + self.init_layoutgrid() + + def _redo_transform_rel_fig(self, bbox=None): + """ + Make the transSubfigure bbox relative to Figure transform. + + Parameters + ---------- + bbox : bbox or None + If not None, then the bbox is used for relative bounding box. + Otherwise it is calculated from the subplotspec. + """ + + if bbox is not None: + self.bbox_relative.p0 = bbox.p0 + self.bbox_relative.p1 = bbox.p1 + return + + gs = self._subplotspec.get_gridspec() + # need to figure out *where* this subplotspec is. + wr = gs.get_width_ratios() + hr = gs.get_height_ratios() + nrows, ncols = gs.get_geometry() + if wr is None: + wr = np.ones(ncols) + else: + wr = np.array(wr) + if hr is None: + hr = np.ones(nrows) + else: + hr = np.array(hr) + widthf = np.sum(wr[self._subplotspec.colspan]) / np.sum(wr) + heightf = np.sum(hr[self._subplotspec.rowspan]) / np.sum(hr) + + x0 = 0 + if not self._subplotspec.is_first_col(): + x0 += np.sum(wr[self._subplotspec.colspan.start - 1]) / np.sum(wr) + + y0 = 0 + if not self._subplotspec.is_last_row(): + y0 += 1 - (np.sum(hr[self._subplotspec.rowspan.stop - 1]) / + np.sum(hr)) + + if self.bbox_relative is None: + self.bbox_relative = Bbox.from_bounds(x0, y0, widthf, heightf) + else: + self.bbox_relative.p0 = (x0, y0) + self.bbox_relative.p1 = (x0 + widthf, y0 + heightf) + + def get_constrained_layout(self): + """ + Return whether constrained layout is being used. + + See :doc:`/tutorials/intermediate/constrainedlayout_guide`. + """ + return self._parent.get_constrained_layout() + + def get_constrained_layout_pads(self, relative=False): + """ + Get padding for ``constrained_layout``. + + Returns a list of ``w_pad, h_pad`` in inches and + ``wspace`` and ``hspace`` as fractions of the subplot. + + See :doc:`/tutorials/intermediate/constrainedlayout_guide`. + + Parameters + ---------- + relative : bool + If `True`, then convert from inches to figure relative. + """ + return self._parent.get_constrained_layout_pads(relative=relative) + + def init_layoutgrid(self): + """Initialize the layoutgrid for use in constrained_layout.""" + if self._layoutgrid is None: + gs = self._subplotspec.get_gridspec() + parent = gs._layoutgrid + if parent is not None: + self._layoutgrid = layoutgrid.LayoutGrid( + parent=parent, + name=(parent.name + '.' + 'panellb' + + layoutgrid.seq_id()), + parent_inner=True, + nrows=1, ncols=1, + parent_pos=(self._subplotspec.rowspan, + self._subplotspec.colspan)) + + def get_axes(self): + """ + Return a list of axes in the SubFigure. You can access and modify the + axes in the Figure through this list. + + Do not modify the list itself. Instead, use `~.SubFigure.add_axes`, + `~.SubFigure.add_subplot` or `~.SubFigure.delaxes` to add or remove an + axes. + + Note: This is equivalent to the property `~.SubFigure.axes`. + """ + return self._localaxes.as_list() + + axes = property(get_axes, doc=""" + List of axes in the SubFigure. You can access and modify the axes + in the SubFigure through this list. + + Do not modify the list itself. Instead, use `~.SubFigure.add_axes`, + `~.SubFigure.add_subplot` or `~.SubFigure.delaxes` to add or remove an + axes. + """) + + def draw(self, renderer): + # docstring inherited + self._cachedRenderer = renderer + + # draw the figure bounding box, perhaps none for white figure + if not self.get_visible(): + return + + artists = self._get_draw_artists(renderer) + + try: + renderer.open_group('subfigure', gid=self.get_gid()) + self.patch.draw(renderer) + mimage._draw_list_compositing_images(renderer, self, artists) + for sfig in self.subfigs: + sfig.draw(renderer) + renderer.close_group('subfigure') + + finally: + self.stale = False + + +class Figure(FigureBase): + """ + The top level container for all the plot elements. + + The Figure instance supports callbacks through a *callbacks* attribute + which is a `.CallbackRegistry` instance. The events you can connect to + are 'dpi_changed', and the callback will be called with ``func(fig)`` where + fig is the `Figure` instance. + + Attributes + ---------- + patch + The `.Rectangle` instance representing the figure background patch. + + suppressComposite + For multiple figure images, the figure will make composite images + depending on the renderer option_image_nocomposite function. If + *suppressComposite* is a boolean, this will override the renderer. + """ + + def __str__(self): + return "Figure(%gx%g)" % tuple(self.bbox.size) + + def __repr__(self): + return "<{clsname} size {h:g}x{w:g} with {naxes} Axes>".format( + clsname=self.__class__.__name__, + h=self.bbox.size[0], w=self.bbox.size[1], + naxes=len(self.axes), + ) + + def __init__(self, + figsize=None, + dpi=None, + facecolor=None, + edgecolor=None, + linewidth=0.0, + frameon=None, + subplotpars=None, # rc figure.subplot.* + tight_layout=None, # rc figure.autolayout + constrained_layout=None, # rc figure.constrained_layout.use + ): + """ + Parameters + ---------- + figsize : 2-tuple of floats, default: :rc:`figure.figsize` + Figure dimension ``(width, height)`` in inches. + + dpi : float, default: :rc:`figure.dpi` + Dots per inch. + + facecolor : default: :rc:`figure.facecolor` + The figure patch facecolor. + + edgecolor : default: :rc:`figure.edgecolor` + The figure patch edge color. + + linewidth : float + The linewidth of the frame (i.e. the edge linewidth of the figure + patch). + + frameon : bool, default: :rc:`figure.frameon` + If ``False``, suppress drawing the figure background patch. + + subplotpars : `SubplotParams` + Subplot parameters. If not given, the default subplot + parameters :rc:`figure.subplot.*` are used. + + tight_layout : bool or dict, default: :rc:`figure.autolayout` + If ``False`` use *subplotpars*. If ``True`` adjust subplot + parameters using `.tight_layout` with default padding. + When providing a dict containing the keys ``pad``, ``w_pad``, + ``h_pad``, and ``rect``, the default `.tight_layout` paddings + will be overridden. + + constrained_layout : bool, default: :rc:`figure.constrained_layout.use` + If ``True`` use constrained layout to adjust positioning of plot + elements. Like ``tight_layout``, but designed to be more + flexible. See + :doc:`/tutorials/intermediate/constrainedlayout_guide` + for examples. (Note: does not work with `add_subplot` or + `~.pyplot.subplot2grid`.) + """ + super().__init__() + + self.callbacks = cbook.CallbackRegistry() + + if figsize is None: + figsize = mpl.rcParams['figure.figsize'] + if dpi is None: + dpi = mpl.rcParams['figure.dpi'] + if facecolor is None: + facecolor = mpl.rcParams['figure.facecolor'] + if edgecolor is None: + edgecolor = mpl.rcParams['figure.edgecolor'] + if frameon is None: + frameon = mpl.rcParams['figure.frameon'] + + if not np.isfinite(figsize).all() or (np.array(figsize) < 0).any(): + raise ValueError('figure size must be positive finite not ' + f'{figsize}') + self.bbox_inches = Bbox.from_bounds(0, 0, *figsize) + + self.dpi_scale_trans = Affine2D().scale(dpi) + # do not use property as it will trigger + self._dpi = dpi + self.bbox = TransformedBbox(self.bbox_inches, self.dpi_scale_trans) + self.figbbox = self.bbox + self.transFigure = BboxTransformTo(self.bbox) + self.transSubfigure = self.transFigure + + self.patch = Rectangle( + xy=(0, 0), width=1, height=1, visible=frameon, + facecolor=facecolor, edgecolor=edgecolor, linewidth=linewidth, + # Don't let the figure patch influence bbox calculation. + in_layout=False) + self._set_artist_props(self.patch) + self.patch.set_antialiased(False) + + FigureCanvasBase(self) # Set self.canvas. + + if subplotpars is None: + subplotpars = SubplotParams() + + self.subplotpars = subplotpars + + # constrained_layout: + self._layoutgrid = None + self._constrained = False + + self.set_tight_layout(tight_layout) + + self._axstack = _AxesStack() # track all figure axes and current axes + self.clf() + self._cachedRenderer = None + + self.set_constrained_layout(constrained_layout) + + # groupers to keep track of x and y labels we want to align. + # see self.align_xlabels and self.align_ylabels and + # axis._get_tick_boxes_siblings + self._align_xlabel_grp = cbook.Grouper() + self._align_ylabel_grp = cbook.Grouper() + + # list of child gridspecs for this figure + self._gridspecs = [] + + # TODO: I'd like to dynamically add the _repr_html_ method + # to the figure in the right context, but then IPython doesn't + # use it, for some reason. + + def _repr_html_(self): + # We can't use "isinstance" here, because then we'd end up importing + # webagg unconditionally. + if 'WebAgg' in type(self.canvas).__name__: + from matplotlib.backends import backend_webagg + return backend_webagg.ipython_inline_display(self) + + def show(self, warn=True): + """ + If using a GUI backend with pyplot, display the figure window. + + If the figure was not created using `~.pyplot.figure`, it will lack + a `~.backend_bases.FigureManagerBase`, and this method will raise an + AttributeError. + + .. warning:: + + This does not manage an GUI event loop. Consequently, the figure + may only be shown briefly or not shown at all if you or your + environment are not managing an event loop. + + Proper use cases for `.Figure.show` include running this from a + GUI application or an IPython shell. + + If you're running a pure python shell or executing a non-GUI + python script, you should use `matplotlib.pyplot.show` instead, + which takes care of managing the event loop for you. + + Parameters + ---------- + warn : bool, default: True + If ``True`` and we are not running headless (i.e. on Linux with an + unset DISPLAY), issue warning when called on a non-GUI backend. + """ + if self.canvas.manager is None: + raise AttributeError( + "Figure.show works only for figures managed by pyplot, " + "normally created by pyplot.figure()") + try: + self.canvas.manager.show() + except NonGuiException as exc: + cbook._warn_external(str(exc)) + + def get_axes(self): + """ + Return a list of axes in the Figure. You can access and modify the + axes in the Figure through this list. + + Do not modify the list itself. Instead, use `~Figure.add_axes`, + `~.Figure.add_subplot` or `~.Figure.delaxes` to add or remove an axes. + + Note: This is equivalent to the property `~.Figure.axes`. + """ + return self._axstack.as_list() + + axes = property(get_axes, doc=""" + List of axes in the Figure. You can access and modify the axes in the + Figure through this list. + + Do not modify the list itself. Instead, use "`~Figure.add_axes`, + `~.Figure.add_subplot` or `~.Figure.delaxes` to add or remove an axes. + """) + + def _get_dpi(self): + return self._dpi + + def _set_dpi(self, dpi, forward=True): + """ + Parameters + ---------- + dpi : float + + forward : bool + Passed on to `~.Figure.set_size_inches` + """ + if dpi == self._dpi: + # We don't want to cause undue events in backends. + return + self._dpi = dpi + self.dpi_scale_trans.clear().scale(dpi) + w, h = self.get_size_inches() + self.set_size_inches(w, h, forward=forward) + self.callbacks.process('dpi_changed', self) + + dpi = property(_get_dpi, _set_dpi, doc="The resolution in dots per inch.") + + def get_tight_layout(self): + """Return whether `.tight_layout` is called when drawing.""" + return self._tight + + def set_tight_layout(self, tight): + """ + Set whether and how `.tight_layout` is called when drawing. + + Parameters + ---------- + tight : bool or dict with keys "pad", "w_pad", "h_pad", "rect" or None + If a bool, sets whether to call `.tight_layout` upon drawing. + If ``None``, use the ``figure.autolayout`` rcparam instead. + If a dict, pass it as kwargs to `.tight_layout`, overriding the + default paddings. + """ + if tight is None: + tight = mpl.rcParams['figure.autolayout'] + self._tight = bool(tight) + self._tight_parameters = tight if isinstance(tight, dict) else {} + self.stale = True + + def get_constrained_layout(self): + """ + Return whether constrained layout is being used. + + See :doc:`/tutorials/intermediate/constrainedlayout_guide`. + """ + return self._constrained + + def set_constrained_layout(self, constrained): + """ + Set whether ``constrained_layout`` is used upon drawing. If None, + :rc:`figure.constrained_layout.use` value will be used. + + When providing a dict containing the keys `w_pad`, `h_pad` + the default ``constrained_layout`` paddings will be + overridden. These pads are in inches and default to 3.0/72.0. + ``w_pad`` is the width padding and ``h_pad`` is the height padding. + + See :doc:`/tutorials/intermediate/constrainedlayout_guide`. + + Parameters + ---------- + constrained : bool or dict or None + """ + self._constrained_layout_pads = dict() + self._constrained_layout_pads['w_pad'] = None + self._constrained_layout_pads['h_pad'] = None + self._constrained_layout_pads['wspace'] = None + self._constrained_layout_pads['hspace'] = None + if constrained is None: + constrained = mpl.rcParams['figure.constrained_layout.use'] + self._constrained = bool(constrained) + if isinstance(constrained, dict): + self.set_constrained_layout_pads(**constrained) + else: + self.set_constrained_layout_pads() + + self.init_layoutgrid() + + self.stale = True + + def set_constrained_layout_pads(self, **kwargs): + """ + Set padding for ``constrained_layout``. Note the kwargs can be passed + as a dictionary ``fig.set_constrained_layout(**paddict)``. + + See :doc:`/tutorials/intermediate/constrainedlayout_guide`. + + Parameters + ---------- + w_pad : float + Width padding in inches. This is the pad around axes + and is meant to make sure there is enough room for fonts to + look good. Defaults to 3 pts = 0.04167 inches + + h_pad : float + Height padding in inches. Defaults to 3 pts. + + wspace : float + Width padding between subplots, expressed as a fraction of the + subplot width. The total padding ends up being w_pad + wspace. + + hspace : float + Height padding between subplots, expressed as a fraction of the + subplot width. The total padding ends up being h_pad + hspace. + + """ + + todo = ['w_pad', 'h_pad', 'wspace', 'hspace'] + for td in todo: + if td in kwargs and kwargs[td] is not None: + self._constrained_layout_pads[td] = kwargs[td] + else: + self._constrained_layout_pads[td] = ( + mpl.rcParams['figure.constrained_layout.' + td]) + + def get_constrained_layout_pads(self, relative=False): + """ + Get padding for ``constrained_layout``. + + Returns a list of ``w_pad, h_pad`` in inches and + ``wspace`` and ``hspace`` as fractions of the subplot. + + See :doc:`/tutorials/intermediate/constrainedlayout_guide`. + + Parameters + ---------- + relative : bool + If `True`, then convert from inches to figure relative. + """ + w_pad = self._constrained_layout_pads['w_pad'] + h_pad = self._constrained_layout_pads['h_pad'] + wspace = self._constrained_layout_pads['wspace'] + hspace = self._constrained_layout_pads['hspace'] + + if relative and (w_pad is not None or h_pad is not None): + renderer0 = layoutgrid.get_renderer(self) + dpi = renderer0.dpi + w_pad = w_pad * dpi / renderer0.width + h_pad = h_pad * dpi / renderer0.height + + return w_pad, h_pad, wspace, hspace + + def set_canvas(self, canvas): + """ + Set the canvas that contains the figure + + Parameters + ---------- + canvas : FigureCanvas + """ + self.canvas = canvas + + def figimage(self, X, xo=0, yo=0, alpha=None, norm=None, cmap=None, + vmin=None, vmax=None, origin=None, resize=False, **kwargs): + """ + Add a non-resampled image to the figure. + + The image is attached to the lower or upper left corner depending on + *origin*. + + Parameters + ---------- + X + The image data. This is an array of one of the following shapes: + + - MxN: luminance (grayscale) values + - MxNx3: RGB values + - MxNx4: RGBA values + + xo, yo : int + The *x*/*y* image offset in pixels. + + alpha : None or float + The alpha blending value. + + norm : `matplotlib.colors.Normalize` + A `.Normalize` instance to map the luminance to the + interval [0, 1]. + + cmap : str or `matplotlib.colors.Colormap`, default: :rc:`image.cmap` + The colormap to use. + + vmin, vmax : float + If *norm* is not given, these values set the data limits for the + colormap. + + origin : {'upper', 'lower'}, default: :rc:`image.origin` + Indicates where the [0, 0] index of the array is in the upper left + or lower left corner of the axes. + + resize : bool + If *True*, resize the figure to match the given image size. + + Returns + ------- + `matplotlib.image.FigureImage` + + Other Parameters + ---------------- + **kwargs + Additional kwargs are `.Artist` kwargs passed on to `.FigureImage`. + + Notes + ----- + figimage complements the axes image (`~matplotlib.axes.Axes.imshow`) + which will be resampled to fit the current axes. If you want + a resampled image to fill the entire figure, you can define an + `~matplotlib.axes.Axes` with extent [0, 0, 1, 1]. + + Examples + -------- + :: + + f = plt.figure() + nx = int(f.get_figwidth() * f.dpi) + ny = int(f.get_figheight() * f.dpi) + data = np.random.random((ny, nx)) + f.figimage(data) + plt.show() + """ + if resize: + dpi = self.get_dpi() + figsize = [x / dpi for x in (X.shape[1], X.shape[0])] + self.set_size_inches(figsize, forward=True) + + im = mimage.FigureImage(self, cmap, norm, xo, yo, origin, **kwargs) + im.stale_callback = _stale_figure_callback + + im.set_array(X) + im.set_alpha(alpha) + if norm is None: + im.set_clim(vmin, vmax) + self.images.append(im) + im._remove_method = self.images.remove + self.stale = True + return im + + def set_size_inches(self, w, h=None, forward=True): + """ + Set the figure size in inches. + + Call signatures:: + + fig.set_size_inches(w, h) # OR + fig.set_size_inches((w, h)) + + Parameters + ---------- + w : (float, float) or float + Width and height in inches (if height not specified as a separate + argument) or width. + h : float + Height in inches. + forward : bool, default: True + If ``True``, the canvas size is automatically updated, e.g., + you can resize the figure window from the shell. + + See Also + -------- + matplotlib.figure.Figure.get_size_inches + matplotlib.figure.Figure.set_figwidth + matplotlib.figure.Figure.set_figheight + + Notes + ----- + To transform from pixels to inches divide by `Figure.dpi`. + """ + if h is None: # Got called with a single pair as argument. + w, h = w + size = np.array([w, h]) + if not np.isfinite(size).all() or (size < 0).any(): + raise ValueError(f'figure size must be positive finite not {size}') + self.bbox_inches.p1 = size + if forward: + canvas = getattr(self, 'canvas') + if canvas is not None: + dpi_ratio = getattr(canvas, '_dpi_ratio', 1) + manager = getattr(canvas, 'manager', None) + if manager is not None: + manager.resize(*(size * self.dpi / dpi_ratio).astype(int)) + self.stale = True + + def get_size_inches(self): + """ + Return the current size of the figure in inches. + + Returns + ------- + ndarray + The size (width, height) of the figure in inches. + + See Also + -------- + matplotlib.figure.Figure.set_size_inches + matplotlib.figure.Figure.get_figwidth + matplotlib.figure.Figure.get_figheight + + Notes + ----- + The size in pixels can be obtained by multiplying with `Figure.dpi`. + """ + return np.array(self.bbox_inches.p1) + + def get_figwidth(self): + """Return the figure width in inches.""" + return self.bbox_inches.width + + def get_figheight(self): + """Return the figure height in inches.""" + return self.bbox_inches.height + + def get_dpi(self): + """Return the resolution in dots per inch as a float.""" + return self.dpi + + def set_dpi(self, val): + """ + Set the resolution of the figure in dots-per-inch. + + Parameters + ---------- + val : float + """ + self.dpi = val + self.stale = True + + def set_figwidth(self, val, forward=True): + """ + Set the width of the figure in inches. - %(Text)s + Parameters + ---------- + val : float + forward : bool + See `set_size_inches`. See Also -------- - .Axes.text - .pyplot.text + matplotlib.figure.Figure.set_figheight + matplotlib.figure.Figure.set_size_inches """ - effective_kwargs = { - 'transform': self.transFigure, - **(fontdict if fontdict is not None else {}), - **kwargs, - } - text = Text(x=x, y=y, text=s, **effective_kwargs) - text.set_figure(self) - text.stale_callback = _stale_figure_callback + self.set_size_inches(val, self.get_figheight(), forward=forward) - self.texts.append(text) - text._remove_method = self.texts.remove - self.stale = True - return text + def set_figheight(self, val, forward=True): + """ + Set the height of the figure in inches. - def _set_artist_props(self, a): - if a != self: - a.set_figure(self) - a.stale_callback = _stale_figure_callback - a.set_transform(self.transFigure) + Parameters + ---------- + val : float + forward : bool + See `set_size_inches`. - @docstring.dedent_interpd - def gca(self, **kwargs): + See Also + -------- + matplotlib.figure.Figure.set_figwidth + matplotlib.figure.Figure.set_size_inches """ - Get the current axes, creating one if necessary. - - The following kwargs are supported for ensuring the returned axes - adheres to the given projection etc., and for axes creation if - the active axes does not exist: + self.set_size_inches(self.get_figwidth(), val, forward=forward) - %(Axes)s + def clf(self, keep_observers=False): + """ + Clear the figure. + Set *keep_observers* to True if, for example, + a gui widget is tracking the axes in the figure. """ - ckey, cax = self._axstack.current_key_axes() - # if there exists an axes on the stack see if it matches - # the desired axes configuration - if cax is not None: + self.suppressComposite = None + self.callbacks = cbook.CallbackRegistry() - # if no kwargs are given just return the current axes - # this is a convenience for gca() on axes such as polar etc. - if not kwargs: - return cax + for ax in tuple(self.axes): # Iterate over the copy. + ax.cla() + self.delaxes(ax) # removes ax from self._axstack - # if the user has specified particular projection detail - # then build up a key which can represent this - else: - projection_class, _, key = \ - self._process_projection_requirements(**kwargs) + toolbar = getattr(self.canvas, 'toolbar', None) + if toolbar is not None: + toolbar.update() + self._axstack.clear() + self.artists = [] + self.lines = [] + self.patches = [] + self.texts = [] + self.images = [] + self.legends = [] + if not keep_observers: + self._axobservers = cbook.CallbackRegistry() + self._suptitle = None + if self.get_constrained_layout(): + self.init_layoutgrid() + self.stale = True - # let the returned axes have any gridspec by removing it from - # the key - ckey = ckey[1:] - key = key[1:] + def clear(self, keep_observers=False): + """Clear the figure -- synonym for `clf`.""" + self.clf(keep_observers=keep_observers) - # if the cax matches this key then return the axes, otherwise - # continue and a new axes will be created - if key == ckey and isinstance(cax, projection_class): - return cax - else: - cbook._warn_external('Requested projection is different ' - 'from current axis projection, ' - 'creating new axis with requested ' - 'projection.') + @_finalize_rasterization + @allow_rasterization + def draw(self, renderer): + # docstring inherited + self._cachedRenderer = renderer - # no axes found, so create one which spans the figure - return self.add_subplot(1, 1, 1, **kwargs) + # draw the figure bounding box, perhaps none for white figure + if not self.get_visible(): + return - def sca(self, a): - """Set the current axes to be *a* and return *a*.""" - self._axstack.bubble(a) - self._axobservers.process("_axes_change_event", self) - return a + artists = self._get_draw_artists(renderer) - def _gci(self): - # Helper for `~matplotlib.pyplot.gci`. Do not use elsewhere. - """ - Get the current colorable artist. + try: + renderer.open_group('figure', gid=self.get_gid()) + if self.get_constrained_layout() and self.axes: + self.execute_constrained_layout(renderer) + if self.get_tight_layout() and self.axes: + try: + self.tight_layout(**self._tight_parameters) + except ValueError: + pass + # ValueError can occur when resizing a window. - Specifically, returns the current `.ScalarMappable` instance (`.Image` - created by `imshow` or `figimage`, `.Collection` created by `pcolor` or - `scatter`, etc.), or *None* if no such instance has been defined. + self.patch.draw(renderer) + mimage._draw_list_compositing_images( + renderer, self, artists, self.suppressComposite) - The current image is an attribute of the current axes, or the nearest - earlier axes in the current figure that contains an image. + for sfig in self.subfigs: + sfig.draw(renderer) - Notes - ----- - Historically, the only colorable artists were images; hence the name - ``gci`` (get current image). + renderer.close_group('figure') + finally: + self.stale = False + + self.canvas.draw_event(renderer) + + def draw_artist(self, a): """ - # Look first for an image in the current Axes: - cax = self._axstack.current_key_axes()[1] - if cax is None: - return None - im = cax._gci() - if im is not None: - return im + Draw `.Artist` instance *a* only. - # If there is no image in the current Axes, search for - # one in a previously created Axes. Whether this makes - # sense is debatable, but it is the documented behavior. - for ax in reversed(self.axes): - im = ax._gci() - if im is not None: - return im - return None + This can only be called after the figure has been drawn. + """ + if self._cachedRenderer is None: + raise AttributeError("draw_artist can only be used after an " + "initial draw which caches the renderer") + a.draw(self._cachedRenderer) def __getstate__(self): state = super().__getstate__() @@ -2364,82 +3062,7 @@ def savefig(self, fname, *, transparent=None, **kwargs): if transparent: for ax, cc in zip(self.axes, original_axes_colors): ax.patch.set_facecolor(cc[0]) - ax.patch.set_edgecolor(cc[1]) - - @docstring.dedent_interpd - def colorbar(self, mappable, cax=None, ax=None, use_gridspec=True, **kw): - """%(colorbar_doc)s""" - if ax is None: - ax = self.gca() - if (hasattr(mappable, "axes") and ax is not mappable.axes - and cax is None): - cbook.warn_deprecated( - "3.4", message="Starting from Matplotlib 3.6, colorbar() " - "will steal space from the mappable's axes, rather than " - "from the current axes, to place the colorbar. To " - "silence this warning, explicitly pass the 'ax' argument " - "to colorbar().") - - # Store the value of gca so that we can set it back later on. - current_ax = self.gca() - - if cax is None: - if (use_gridspec and isinstance(ax, SubplotBase) - and not self.get_constrained_layout()): - cax, kw = cbar.make_axes_gridspec(ax, **kw) - else: - cax, kw = cbar.make_axes(ax, **kw) - - # need to remove kws that cannot be passed to Colorbar - NON_COLORBAR_KEYS = ['fraction', 'pad', 'shrink', 'aspect', 'anchor', - 'panchor'] - cb_kw = {k: v for k, v in kw.items() if k not in NON_COLORBAR_KEYS} - cb = cbar.Colorbar(cax, mappable, **cb_kw) - - self.sca(current_ax) - self.stale = True - return cb - - def subplots_adjust(self, left=None, bottom=None, right=None, top=None, - wspace=None, hspace=None): - """ - Adjust the subplot layout parameters. - - Unset parameters are left unmodified; initial values are given by - :rc:`figure.subplot.[name]`. - - Parameters - ---------- - left : float, optional - The position of the left edge of the subplots, - as a fraction of the figure width. - right : float, optional - The position of the right edge of the subplots, - as a fraction of the figure width. - bottom : float, optional - The position of the bottom edge of the subplots, - as a fraction of the figure height. - top : float, optional - The position of the top edge of the subplots, - as a fraction of the figure height. - wspace : float, optional - The width of the padding between subplots, - as a fraction of the average axes width. - hspace : float, optional - The height of the padding between subplots, - as a fraction of the average axes height. - """ - if self.get_constrained_layout(): - self.set_constrained_layout(False) - cbook._warn_external( - "This figure was using constrained_layout, but that is " - "incompatible with subplots_adjust and/or tight_layout; " - "disabling constrained_layout.") - self.subplotpars.update(left, bottom, right, top, wspace, hspace) - for ax in self.axes: - if isinstance(ax, SubplotBase): - ax._set_position(ax.get_subplotspec().get_position(self)) - self.stale = True + ax.patch.set_edgecolor(cc[1]) def ginput(self, n=1, timeout=30, show_clicks=True, mouse_add=MouseButton.LEFT, @@ -2508,72 +3131,6 @@ def waitforbuttonpress(self, timeout=-1): blocking_input = BlockingKeyMouseInput(self) return blocking_input(timeout=timeout) - def get_default_bbox_extra_artists(self): - bbox_artists = [artist for artist in self.get_children() - if (artist.get_visible() and artist.get_in_layout())] - for ax in self.axes: - if ax.get_visible(): - bbox_artists.extend(ax.get_default_bbox_extra_artists()) - return bbox_artists - - def get_tightbbox(self, renderer, bbox_extra_artists=None): - """ - Return a (tight) bounding box of the figure in inches. - - Artists that have ``artist.set_in_layout(False)`` are not included - in the bbox. - - Parameters - ---------- - renderer : `.RendererBase` subclass - renderer that will be used to draw the figures (i.e. - ``fig.canvas.get_renderer()``) - - bbox_extra_artists : list of `.Artist` or ``None`` - List of artists to include in the tight bounding box. If - ``None`` (default), then all artist children of each axes are - included in the tight bounding box. - - Returns - ------- - `.BboxBase` - containing the bounding box (in figure inches). - """ - - bb = [] - if bbox_extra_artists is None: - artists = self.get_default_bbox_extra_artists() - else: - artists = bbox_extra_artists - - for a in artists: - bbox = a.get_tightbbox(renderer) - if bbox is not None and (bbox.width != 0 or bbox.height != 0): - bb.append(bbox) - - for ax in self.axes: - if ax.get_visible(): - # some axes don't take the bbox_extra_artists kwarg so we - # need this conditional.... - try: - bbox = ax.get_tightbbox( - renderer, bbox_extra_artists=bbox_extra_artists) - except TypeError: - bbox = ax.get_tightbbox(renderer) - bb.append(bbox) - bb = [b for b in bb - if (np.isfinite(b.width) and np.isfinite(b.height) - and (b.width != 0 or b.height != 0))] - - if len(bb) == 0: - return self.bbox_inches - - _bbox = Bbox.union(bb) - - bbox_inches = TransformedBbox(_bbox, Affine2D().scale(1 / self.dpi)) - - return bbox_inches - def init_layoutgrid(self): """Initialize the layoutgrid for use in constrained_layout.""" del(self._layoutgrid) @@ -2660,195 +3217,6 @@ def tight_layout(self, renderer=None, pad=1.08, h_pad=None, w_pad=None, if kwargs: self.subplots_adjust(**kwargs) - def align_xlabels(self, axs=None): - """ - Align the xlabels of subplots in the same subplot column if label - alignment is being done automatically (i.e. the label position is - not manually set). - - Alignment persists for draw events after this is called. - - If a label is on the bottom, it is aligned with labels on axes that - also have their label on the bottom and that have the same - bottom-most subplot row. If the label is on the top, - it is aligned with labels on axes with the same top-most row. - - Parameters - ---------- - axs : list of `~matplotlib.axes.Axes` - Optional list of (or ndarray) `~matplotlib.axes.Axes` - to align the xlabels. - Default is to align all axes on the figure. - - See Also - -------- - matplotlib.figure.Figure.align_ylabels - matplotlib.figure.Figure.align_labels - - Notes - ----- - This assumes that ``axs`` are from the same `.GridSpec`, so that - their `.SubplotSpec` positions correspond to figure positions. - - Examples - -------- - Example with rotated xtick labels:: - - fig, axs = plt.subplots(1, 2) - for tick in axs[0].get_xticklabels(): - tick.set_rotation(55) - axs[0].set_xlabel('XLabel 0') - axs[1].set_xlabel('XLabel 1') - fig.align_xlabels() - """ - if axs is None: - axs = self.axes - axs = np.ravel(axs) - for ax in axs: - _log.debug(' Working on: %s', ax.get_xlabel()) - rowspan = ax.get_subplotspec().rowspan - pos = ax.xaxis.get_label_position() # top or bottom - # Search through other axes for label positions that are same as - # this one and that share the appropriate row number. - # Add to a grouper associated with each axes of siblings. - # This list is inspected in `axis.draw` by - # `axis._update_label_position`. - for axc in axs: - if axc.xaxis.get_label_position() == pos: - rowspanc = axc.get_subplotspec().rowspan - if (pos == 'top' and rowspan.start == rowspanc.start or - pos == 'bottom' and rowspan.stop == rowspanc.stop): - # grouper for groups of xlabels to align - self._align_xlabel_grp.join(ax, axc) - - def align_ylabels(self, axs=None): - """ - Align the ylabels of subplots in the same subplot column if label - alignment is being done automatically (i.e. the label position is - not manually set). - - Alignment persists for draw events after this is called. - - If a label is on the left, it is aligned with labels on axes that - also have their label on the left and that have the same - left-most subplot column. If the label is on the right, - it is aligned with labels on axes with the same right-most column. - - Parameters - ---------- - axs : list of `~matplotlib.axes.Axes` - Optional list (or ndarray) of `~matplotlib.axes.Axes` - to align the ylabels. - Default is to align all axes on the figure. - - See Also - -------- - matplotlib.figure.Figure.align_xlabels - matplotlib.figure.Figure.align_labels - - Notes - ----- - This assumes that ``axs`` are from the same `.GridSpec`, so that - their `.SubplotSpec` positions correspond to figure positions. - - Examples - -------- - Example with large yticks labels:: - - fig, axs = plt.subplots(2, 1) - axs[0].plot(np.arange(0, 1000, 50)) - axs[0].set_ylabel('YLabel 0') - axs[1].set_ylabel('YLabel 1') - fig.align_ylabels() - """ - if axs is None: - axs = self.axes - axs = np.ravel(axs) - for ax in axs: - _log.debug(' Working on: %s', ax.get_ylabel()) - colspan = ax.get_subplotspec().colspan - pos = ax.yaxis.get_label_position() # left or right - # Search through other axes for label positions that are same as - # this one and that share the appropriate column number. - # Add to a list associated with each axes of siblings. - # This list is inspected in `axis.draw` by - # `axis._update_label_position`. - for axc in axs: - if axc.yaxis.get_label_position() == pos: - colspanc = axc.get_subplotspec().colspan - if (pos == 'left' and colspan.start == colspanc.start or - pos == 'right' and colspan.stop == colspanc.stop): - # grouper for groups of ylabels to align - self._align_ylabel_grp.join(ax, axc) - - def align_labels(self, axs=None): - """ - Align the xlabels and ylabels of subplots with the same subplots - row or column (respectively) if label alignment is being - done automatically (i.e. the label position is not manually set). - - Alignment persists for draw events after this is called. - - Parameters - ---------- - axs : list of `~matplotlib.axes.Axes` - Optional list (or ndarray) of `~matplotlib.axes.Axes` - to align the labels. - Default is to align all axes on the figure. - - See Also - -------- - matplotlib.figure.Figure.align_xlabels - - matplotlib.figure.Figure.align_ylabels - """ - self.align_xlabels(axs=axs) - self.align_ylabels(axs=axs) - - def add_gridspec(self, nrows=1, ncols=1, **kwargs): - """ - Return a `.GridSpec` that has this figure as a parent. This allows - complex layout of axes in the figure. - - Parameters - ---------- - nrows : int, default: 1 - Number of rows in grid. - - ncols : int, default: 1 - Number or columns in grid. - - Returns - ------- - `.GridSpec` - - Other Parameters - ---------------- - **kwargs - Keyword arguments are passed to `.GridSpec`. - - See Also - -------- - matplotlib.pyplot.subplots - - Examples - -------- - Adding a subplot that spans two rows:: - - fig = plt.figure() - gs = fig.add_gridspec(2, 2) - ax1 = fig.add_subplot(gs[0, 0]) - ax2 = fig.add_subplot(gs[1, 0]) - # spans two rows: - ax3 = fig.add_subplot(gs[:, 1]) - - """ - - _ = kwargs.pop('figure', None) # pop in case user has added this... - gs = GridSpec(nrows=nrows, ncols=ncols, figure=self, **kwargs) - self._gridspecs.append(gs) - return gs - def figaspect(arg): """ diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 8dcaea286db5..fa2b0da16448 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -4276,34 +4276,43 @@ def __init__(self, xyA, xyB, coordsA, coordsB=None, *coordsA* and *coordsB* are strings that indicate the coordinates of *xyA* and *xyB*. - ================= =================================================== - Property Description - ================= =================================================== - 'figure points' points from the lower left corner of the figure - 'figure pixels' pixels from the lower left corner of the figure - 'figure fraction' 0, 0 is lower left of figure and 1, 1 is upper right - 'axes points' points from lower left corner of axes - 'axes pixels' pixels from lower left corner of axes - 'axes fraction' 0, 0 is lower left of axes and 1, 1 is upper right - 'data' use the coordinate system of the object being - annotated (default) - 'offset points' offset (in points) from the *xy* value - 'polar' you can specify *theta*, *r* for the annotation, - even in cartesian plots. Note that if you are using - a polar axes, you do not need to specify polar for - the coordinate system since that is the native - "data" coordinate system. - ================= =================================================== + ==================== ================================================== + Property Description + ==================== ================================================== + 'figure points' points from the lower left corner of the figure + 'figure pixels' pixels from the lower left corner of the figure + 'figure fraction' 0, 0 is lower left of figure and 1, 1 is upper + right + 'subfigure points' points from the lower left corner of the subfigure + 'subfigure pixels' pixels from the lower left corner of the subfigure + 'subfigure fraction' fraction of the subfigure, 0, 0 is lower left. + 'axes points' points from lower left corner of axes + 'axes pixels' pixels from lower left corner of axes + 'axes fraction' 0, 0 is lower left of axes and 1, 1 is upper right + 'data' use the coordinate system of the object being + annotated (default) + 'offset points' offset (in points) from the *xy* value + 'polar' you can specify *theta*, *r* for the annotation, + even in cartesian plots. Note that if you are + using a polar axes, you do not need to specify + polar for the coordinate system since that is the + native "data" coordinate system. + ==================== ================================================== Alternatively they can be set to any valid `~matplotlib.transforms.Transform`. + Note that 'subfigure pixels' and 'figure pixels' are the same + for the parent figure, so users who want code that is usable in + a subfigure can use 'subfigure pixels'. + .. note:: Using `ConnectionPatch` across two `~.axes.Axes` instances is not directly compatible with :doc:`constrained layout `. Add the artist - directly to the `.Figure` instead of adding it to a specific Axes. + directly to the `.Figure` instead of adding it to a specific Axes, + or exclude it from the layout using ``con.set_in_layout(False)``. .. code-block:: default @@ -4348,6 +4357,8 @@ def _get_xy(self, xy, s, axes=None): s = s.replace("points", "pixels") elif s == "figure fraction": s = self.figure.transFigure + elif s == "subfigure fraction": + s = self.figure.transSubfigure elif s == "axes fraction": s = axes.transAxes x, y = xy @@ -4370,6 +4381,12 @@ def _get_xy(self, xy, s, axes=None): trans = axes.transData return trans.transform((x, y)) elif s == 'figure pixels': + # pixels from the lower left corner of the figure + bb = self.figure.figbbox + x = bb.x0 + x if x >= 0 else bb.x1 + x + y = bb.y0 + y if y >= 0 else bb.y1 + y + return x, y + elif s == 'subfigure pixels': # pixels from the lower left corner of the figure bb = self.figure.bbox x = bb.x0 + x if x >= 0 else bb.x1 + x diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index e177d727d142..527aac59b6ed 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -1068,8 +1068,8 @@ def subplot(*args, **kwargs): """ Add a subplot to the current figure. - Wrapper of `.Figure.add_subplot` with a difference in behavior - explained in the notes section. + Wrapper of `.Figure.add_subplot` with a difference in + behavior explained in the notes section. Call signatures:: @@ -1159,7 +1159,7 @@ def subplot(*args, **kwargs): two subplots that are otherwise identical to be added to the figure, make sure you give them unique labels. - In rare circumstances, `.add_subplot` may be called with a single + In rare circumstances, `.Figure.add_subplot` may be called with a single argument, a subplot axes instance already created in the present figure but not in the figure's list of axes. diff --git a/lib/matplotlib/tests/baseline_images/test_figure/test_subfigure.png b/lib/matplotlib/tests/baseline_images/test_figure/test_subfigure.png new file mode 100644 index 000000000000..2e7e43ec6ba0 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_figure/test_subfigure.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_figure/test_subfigure_ss.png b/lib/matplotlib/tests/baseline_images/test_figure/test_subfigure_ss.png new file mode 100644 index 000000000000..62342ccf9591 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_figure/test_subfigure_ss.png differ diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index fbddcfe7876d..ab90a96792bd 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -810,3 +810,50 @@ def test_reused_gridspec(): assert gs1 == gs2 assert gs1 == gs3 + + +@image_comparison(['test_subfigure.png'], style='mpl20', + savefig_kwarg={'facecolor': 'teal'}, + remove_text=False) +def test_subfigure(): + np.random.seed(19680808) + fig = plt.figure(constrained_layout=True) + sub = fig.subfigures(1, 2) + + axs = sub[0].subplots(2, 2) + for ax in axs.flat: + pc = ax.pcolormesh(np.random.randn(30, 30), vmin=-2, vmax=2) + sub[0].colorbar(pc, ax=axs) + sub[0].suptitle('Left Side') + + axs = sub[1].subplots(1, 3) + for ax in axs.flat: + pc = ax.pcolormesh(np.random.randn(30, 30), vmin=-2, vmax=2) + sub[1].colorbar(pc, ax=axs, location='bottom') + sub[1].suptitle('Right Side') + + fig.suptitle('Figure suptitle', fontsize='xx-large') + + +@image_comparison(['test_subfigure_ss.png'], style='mpl20', + savefig_kwarg={'facecolor': 'teal'}, + remove_text=False) +def test_subfigure_ss(): + # test assigning the subfigure via subplotspec + np.random.seed(19680808) + fig = plt.figure(constrained_layout=True) + gs = fig.add_gridspec(1, 2) + + sub = fig.add_subfigure(gs[0], facecolor='pink') + + axs = sub.subplots(2, 2) + for ax in axs.flat: + pc = ax.pcolormesh(np.random.randn(30, 30), vmin=-2, vmax=2) + sub.colorbar(pc, ax=axs) + sub.suptitle('Left Side') + + ax = fig.add_subplot(gs[1]) + ax.plot(np.arange(20)) + ax.set_title('Axes') + + fig.suptitle('Figure suptitle', fontsize='xx-large') diff --git a/lib/matplotlib/tests/test_polar.py b/lib/matplotlib/tests/test_polar.py index 9f50c6890400..768d66873e58 100644 --- a/lib/matplotlib/tests/test_polar.py +++ b/lib/matplotlib/tests/test_polar.py @@ -1,5 +1,3 @@ -import platform - import numpy as np from numpy.testing import assert_allclose import pytest @@ -9,8 +7,7 @@ from matplotlib.testing.decorators import image_comparison, check_figures_equal -@image_comparison(['polar_axes'], style='default', - tol=0 if platform.machine() == 'x86_64' else 0.01) +@image_comparison(['polar_axes'], style='default', tol=0.012) def test_polar_annotations(): # You can specify the xypoint and the xytext in different positions and # coordinate systems, and optionally turn on a connecting line and mark the @@ -44,7 +41,8 @@ def test_polar_annotations(): ax.tick_params(axis='x', tick1On=True, tick2On=True, direction='out') -@image_comparison(['polar_coords'], style='default', remove_text=True) +@image_comparison(['polar_coords'], style='default', remove_text=True, + tol=0.012) def test_polar_coord_annotations(): # You can also use polar notation on a cartesian axes. Here the native # coordinate system ('data') is cartesian, so you need to specify the diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index 0007a3babc06..dc7824f3204d 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -1428,6 +1428,8 @@ def _get_xy_transform(self, renderer, s): bbox_name, unit = s_ # if unit is offset-like if bbox_name == "figure": + bbox0 = self.figure.figbbox + elif bbox_name == "subfigure": bbox0 = self.figure.bbox elif bbox_name == "axes": bbox0 = self.axes.bbox @@ -1611,19 +1613,27 @@ def __init__(self, text, xy, - One of the following strings: - ================= ============================================= - Value Description - ================= ============================================= - 'figure points' Points from the lower left of the figure - 'figure pixels' Pixels from the lower left of the figure - 'figure fraction' Fraction of figure from lower left - 'axes points' Points from lower left corner of axes - 'axes pixels' Pixels from lower left corner of axes - 'axes fraction' Fraction of axes from lower left - 'data' Use the coordinate system of the object being - annotated (default) - 'polar' *(theta, r)* if not native 'data' coordinates - ================= ============================================= + ==================== ============================================ + Value Description + ==================== ============================================ + 'figure points' Points from the lower left of the figure + 'figure pixels' Pixels from the lower left of the figure + 'figure fraction' Fraction of figure from lower left + 'subfigure points' Points from the lower left of the subfigure + 'subfigure pixels' Pixels from the lower left of the subfigure + 'subfigure fraction' Fraction of subfigure from lower left + 'axes points' Points from lower left corner of axes + 'axes pixels' Pixels from lower left corner of axes + 'axes fraction' Fraction of axes from lower left + 'data' Use the coordinate system of the object + being annotated (default) + 'polar' *(theta, r)* if not native 'data' + coordinates + ==================== ============================================ + + Note that 'subfigure pixels' and 'figure pixels' are the same + for the parent figure, so users who want code that is usable in + a subfigure can use 'subfigure pixels'. - An `.Artist`: *xy* is interpreted as a fraction of the artist's `~matplotlib.transforms.Bbox`. E.g. *(0, 0)* would be the lower diff --git a/lib/mpl_toolkits/axes_grid1/colorbar.py b/lib/mpl_toolkits/axes_grid1/colorbar.py index 97cf11c45f51..77af5a1ab7fc 100644 --- a/lib/mpl_toolkits/axes_grid1/colorbar.py +++ b/lib/mpl_toolkits/axes_grid1/colorbar.py @@ -45,8 +45,8 @@ *fraction* 0.15; fraction of original axes to use for colorbar *pad* Defaults to 0.05 if vertical, 0.15 if horizontal; fraction of original axes between colorbar and new image axes. - Defaults to 0.05 for both if `.get_constrained_layout` - is *True*. + Defaults to 0.05 for both if + `.Figure.get_constrained_layout` is *True*. *shrink* 1.0; fraction by which to shrink the colorbar *aspect* 20; ratio of long to short dimensions ============= ==================================================== diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 92c4f2ba927e..32243f5659ab 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -377,8 +377,11 @@ def apply_aspect(self, position=None): # in the superclass, we would go through and actually deal with axis # scales and box/datalim. Those are all irrelevant - all we need to do # is make sure our coordinate system is square. - figW, figH = self.get_figure().get_size_inches() - fig_aspect = figH / figW + trans = self.get_figure().transSubfigure + bb = mtransforms.Bbox.from_bounds(0, 0, 1, 1).transformed(trans) + # this is the physical aspect of the panel (or figure): + fig_aspect = bb.height / bb.width + box_aspect = 1 pb = position.frozen() pb1 = pb.shrunk_to_aspect(box_aspect, pb, fig_aspect) diff --git a/lib/mpl_toolkits/tests/test_mplot3d.py b/lib/mpl_toolkits/tests/test_mplot3d.py index 5ac5b1e62e4d..e638614d4185 100644 --- a/lib/mpl_toolkits/tests/test_mplot3d.py +++ b/lib/mpl_toolkits/tests/test_mplot3d.py @@ -371,7 +371,7 @@ def test_marker_draw_order_view_rotated(fig_test, fig_ref): ax.view_init(elev=0, azim=azim - 180) # view rotated by 180 degrees -@mpl3d_image_comparison(['plot_3d_from_2d.png'], tol=0.01) +@mpl3d_image_comparison(['plot_3d_from_2d.png'], tol=0.015) def test_plot_3d_from_2d(): fig = plt.figure() ax = fig.add_subplot(projection='3d') diff --git a/tutorials/advanced/transforms_tutorial.py b/tutorials/advanced/transforms_tutorial.py index 09bedc428814..3fe230f3fd6a 100644 --- a/tutorials/advanced/transforms_tutorial.py +++ b/tutorials/advanced/transforms_tutorial.py @@ -29,6 +29,13 @@ | | |is bottom left of the axes, and | | | |(1, 1) is top right of the axes. | +----------------+-----------------------------+-----------------------------------+ +|"subfigure" |``subfigure.transSubfigure`` |The coordinate system of the | +| | |`.SubFigure`; (0, 0) is bottom left| +| | |of the subfigure, and (1, 1) is top| +| | |right of the subfigure. If a | +| | |figure has no subfigures, this is | +| | |the same as ``transFigure``. | ++----------------+-----------------------------+-----------------------------------+ |"figure" |``fig.transFigure`` |The coordinate system of the | | | |`.Figure`; (0, 0) is bottom left | | | |of the figure, and (1, 1) is top |