Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 454f97f

Browse files
committed
ENH: Subfigures
1 parent dd15dc0 commit 454f97f

File tree

21 files changed

+2219
-1642
lines changed

21 files changed

+2219
-1642
lines changed

doc/api/figure_api.rst

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,5 @@
55
.. currentmodule:: matplotlib.figure
66

77
.. automodule:: matplotlib.figure
8-
:no-members:
9-
:no-inherited-members:
10-
11-
Classes
12-
-------
13-
14-
.. autosummary::
15-
:toctree: _as_gen/
16-
:template: autosummary.rst
17-
:nosignatures:
18-
19-
Figure
20-
SubplotParams
21-
22-
Functions
23-
---------
24-
25-
.. autosummary::
26-
:toctree: _as_gen/
27-
:template: autosummary.rst
28-
:nosignatures:
29-
30-
figaspect
8+
:members:
9+
:inherited-members:
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
FigureBase class added, and Figure class made a child
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
4+
The new subfigure feature motivated some re-organization of the
5+
`.figure.Figure` class, so that the new `.figure.SubFigure` class could have
6+
all the capabilities of a figure.
7+
8+
The `.figure.Figure` class is now a subclass of `.figure.FigureBase`, where
9+
`.figure.FigureBase` contains figure-level artist addition routines, and
10+
the `.figure.Figure` subclass just contains features that are unique to the
11+
outer figure.
12+
13+
Note that there are a new *transSubfigure* transform
14+
associated with the subfigure. This transform also exists for a
15+
`.Figure` instance, and is equal to *transFigure* in that case,
16+
so code that uses the transform stack that wants to place objects on either
17+
the parent figure or one of the subfigures should use *transSubfigure*.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
New subfigure functionality
2+
---------------------------
3+
New `.figure.Figure.add_subfigure` and `.figure.Figure.subfigures`
4+
functionalities allow creating virtual figures within figures. Similar
5+
nesting was previously done with nested gridspecs
6+
( see :doc:`/gallery/subplots_axes_and_figures/gridspec_nested`). However, this
7+
did not allow localized figure artists (i.e. a colorbar or suptitle) that
8+
only pertained to each subgridspec.
9+
10+
The new methods `.figure.Figure.add_subfigure` and `.figure.Figure.subfigures`
11+
are meant to rhyme with `.figure.Figure.add_subplot` and
12+
`.figure.Figure.subplots` and have most of the same arguments.
13+
14+
See :doc:`/gallery/subplots_axes_and_figures/subfigures`.
15+
16+
.. note::
17+
18+
The subfigure functionality is experimental API as of v3.4.
19+
20+
.. plot::
21+
22+
def example_plot(ax, fontsize=12, hide_labels=False):
23+
pc = ax.pcolormesh(np.random.randn(30, 30))
24+
if not hide_labels:
25+
ax.set_xlabel('x-label', fontsize=fontsize)
26+
ax.set_ylabel('y-label', fontsize=fontsize)
27+
ax.set_title('Title', fontsize=fontsize)
28+
return pc
29+
30+
31+
fig = plt.figure(constrained_layout=True, figsize=(10, 4))
32+
subfigs = fig.subfigures(1, 2, wspace=0.07)
33+
34+
axsLeft = subfigs[0].subplots(1, 2, sharey=True)
35+
subfigs[0].set_facecolor('0.75')
36+
for ax in axsLeft:
37+
pc = example_plot(ax)
38+
subfigs[0].suptitle('Left plots', fontsize='x-large')
39+
subfigs[0].colorbar(pc, shrink=0.6, ax=axsLeft, location='bottom')
40+
41+
axsRight = subfigs[1].subplots(3, 1, sharex=True)
42+
for nn, ax in enumerate(axsRight):
43+
pc = example_plot(ax, hide_labels=True)
44+
if nn == 2:
45+
ax.set_xlabel('xlabel')
46+
if nn == 1:
47+
ax.set_ylabel('ylabel')
48+
subfigs[1].colorbar(pc, shrink=0.6, ax=axsRight)
49+
subfigs[1].suptitle('Right plots', fontsize='x-large')
50+
51+
fig.suptitle('Figure suptitle', fontsize='xx-large')
52+
53+
plt.show()

examples/subplots_axes_and_figures/gridspec_nested.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
GridSpecs can be nested, so that a subplot from a parent GridSpec can
77
set the position for a nested grid of subplots.
88
9+
Note that the same functionality can be achieved more directly with
10+
`~.figure.FigureBase.subfigures`:
11+
:doc:`nested gridspecs</gallery/subplots_axes_and_figures/subfigures>`,
12+
913
"""
1014
import matplotlib.pyplot as plt
1115
import matplotlib.gridspec as gridspec
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
"""
2+
=================
3+
Figure subfigures
4+
=================
5+
6+
Sometimes it is desirable to have a figure with two different layouts in it.
7+
This can be achieved with
8+
:doc:`nested gridspecs</gallery/subplots_axes_and_figures/gridspec_nested>`,
9+
but having a virtual figure with its own artists is helpful, so
10+
Matplotlib also has "subfigures", accessed by calling
11+
`matplotlib.figure.Figure.add_subfigure` in a way that is analagous to
12+
`matplotlib.figure.Figure.add_subplot`, or
13+
`matplotlib.figure.Figure.subfigures` to make an array of subfigures. Note
14+
that subfigures can also have their own child subfigures.
15+
16+
.. note::
17+
``subfigure`` is new in v3.4, and the API is still provisional.
18+
19+
"""
20+
import matplotlib.pyplot as plt
21+
import numpy as np
22+
23+
24+
def example_plot(ax, fontsize=12, hide_labels=False):
25+
pc = ax.pcolormesh(np.random.randn(30, 30), vmin=-2.5, vmax=2.5)
26+
if not hide_labels:
27+
ax.set_xlabel('x-label', fontsize=fontsize)
28+
ax.set_ylabel('y-label', fontsize=fontsize)
29+
ax.set_title('Title', fontsize=fontsize)
30+
return pc
31+
32+
# gridspec inside gridspec
33+
fig = plt.figure(constrained_layout=True, figsize=(10, 4))
34+
subfigs = fig.subfigures(1, 2, wspace=0.07)
35+
36+
axsLeft = subfigs[0].subplots(1, 2, sharey=True)
37+
subfigs[0].set_facecolor('0.75')
38+
for ax in axsLeft:
39+
pc = example_plot(ax)
40+
subfigs[0].suptitle('Left plots', fontsize='x-large')
41+
subfigs[0].colorbar(pc, shrink=0.6, ax=axsLeft, location='bottom')
42+
43+
axsRight = subfigs[1].subplots(3, 1, sharex=True)
44+
for nn, ax in enumerate(axsRight):
45+
pc = example_plot(ax, hide_labels=True)
46+
if nn == 2:
47+
ax.set_xlabel('xlabel')
48+
if nn == 1:
49+
ax.set_ylabel('ylabel')
50+
51+
subfigs[1].set_facecolor('0.85')
52+
subfigs[1].colorbar(pc, shrink=0.6, ax=axsRight)
53+
subfigs[1].suptitle('Right plots', fontsize='x-large')
54+
55+
fig.suptitle('Figure suptitle', fontsize='xx-large')
56+
57+
plt.show()
58+
59+
##############################################################################
60+
# It is possible to mix subplots and subfigures using
61+
# `matplotlib.figure.Figure.add_subfigure`. This requires getting
62+
# the gridspec that the subplots are laid out on.
63+
64+
fig, axs = plt.subplots(2, 3, constrained_layout=True, figsize=(10, 4))
65+
66+
# clear the left column for the subfigure:
67+
for a in axs[:, 0]:
68+
gridspec = a.get_subplotspec().get_gridspec()
69+
a.remove()
70+
71+
# plot data in remaining axes:
72+
for a in axs[:, 1:].flat:
73+
a.plot(np.arange(10))
74+
75+
# make the subfigure in the empy gridspec slots:
76+
subfig = fig.add_subfigure(gridspec[:, 0])
77+
78+
axsLeft = subfig.subplots(1, 2, sharey=True)
79+
subfig.set_facecolor('0.75')
80+
for ax in axsLeft:
81+
pc = example_plot(ax)
82+
subfig.suptitle('Left plots', fontsize='x-large')
83+
subfig.colorbar(pc, shrink=0.6, ax=axsLeft, location='bottom')
84+
85+
fig.suptitle('Figure suptitle', fontsize='xx-large')
86+
plt.show()
87+
88+
##############################################################################
89+
# Subfigures can have different widths and heights. This is exactly the
90+
# same example as the first example, but *width_ratios* has been changed:
91+
92+
fig = plt.figure(constrained_layout=True, figsize=(10, 4))
93+
subfigs = fig.subfigures(1, 2, wspace=0.07, width_ratios=[2, 1])
94+
95+
axsLeft = subfigs[0].subplots(1, 2, sharey=True)
96+
subfigs[0].set_facecolor('0.75')
97+
for ax in axsLeft:
98+
pc = example_plot(ax)
99+
subfigs[0].suptitle('Left plots', fontsize='x-large')
100+
subfigs[0].colorbar(pc, shrink=0.6, ax=axsLeft, location='bottom')
101+
102+
axsRight = subfigs[1].subplots(3, 1, sharex=True)
103+
for nn, ax in enumerate(axsRight):
104+
pc = example_plot(ax, hide_labels=True)
105+
if nn == 2:
106+
ax.set_xlabel('xlabel')
107+
if nn == 1:
108+
ax.set_ylabel('ylabel')
109+
110+
subfigs[1].set_facecolor('0.85')
111+
subfigs[1].colorbar(pc, shrink=0.6, ax=axsRight)
112+
subfigs[1].suptitle('Right plots', fontsize='x-large')
113+
114+
fig.suptitle('Figure suptitle', fontsize='xx-large')
115+
116+
plt.show()
117+
118+
##############################################################################
119+
# Subfigures can be also be nested:
120+
121+
fig = plt.figure(constrained_layout=True, figsize=(10, 8))
122+
123+
fig.suptitle('fig')
124+
125+
subfigs = fig.subfigures(1, 2, wspace=0.07)
126+
127+
subfigs[0].set_facecolor('coral')
128+
subfigs[0].suptitle('subfigs[0]')
129+
130+
subfigs[1].set_facecolor('coral')
131+
subfigs[1].suptitle('subfigs[1]')
132+
133+
subfigsnest = subfigs[0].subfigures(2, 1, height_ratios=[1, 1.4])
134+
subfigsnest[0].suptitle('subfigsnest[0]')
135+
subfigsnest[0].set_facecolor('r')
136+
axsnest0 = subfigsnest[0].subplots(1, 2, sharey=True)
137+
for nn, ax in enumerate(axsnest0):
138+
pc = example_plot(ax, hide_labels=True)
139+
subfigsnest[0].colorbar(pc, ax=axsnest0)
140+
141+
subfigsnest[1].suptitle('subfigsnest[1]')
142+
subfigsnest[1].set_facecolor('g')
143+
axsnest1 = subfigsnest[1].subplots(3, 1, sharex=True)
144+
145+
axsRight = subfigs[1].subplots(2, 2)
146+
147+
plt.show()

examples/subplots_axes_and_figures/subplots_demo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@
144144
# Still there remains an unused empty space between the subplots.
145145
#
146146
# To precisely control the positioning of the subplots, one can explicitly
147-
# create a `.GridSpec` with `.add_gridspec`, and then call its
147+
# create a `.GridSpec` with `.Figure.add_gridspec`, and then call its
148148
# `~.GridSpecBase.subplots` method. For example, we can reduce the height
149149
# between vertical subplots using ``add_gridspec(hspace=0)``.
150150
#

lib/matplotlib/_constrained_layout.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,10 @@ def do_constrained_layout(fig, renderer, h_pad, w_pad,
103103
# larger/smaller). This second reposition tends to be much milder,
104104
# so doing twice makes things work OK.
105105

106-
# make margins for all the axes and subpanels in the
106+
# make margins for all the axes and subfigures in the
107107
# figure. Add margins for colorbars...
108108
_make_layout_margins(fig, renderer, h_pad=h_pad, w_pad=w_pad,
109109
hspace=hspace, wspace=wspace)
110-
111110
_make_margin_suptitles(fig, renderer, h_pad=h_pad, w_pad=w_pad)
112111

113112
# if a layout is such that a columns (or rows) margin has no
@@ -207,8 +206,7 @@ def _make_layout_margins(fig, renderer, *, w_pad=0, h_pad=0,
207206
hspace=hspace, wspace=wspace)
208207
panel._layoutgrid.parent.edit_outer_margin_mins(margins, ss)
209208

210-
# for ax in [a for a in fig._localaxes if hasattr(a, 'get_subplotspec')]:
211-
for ax in fig.get_axes():
209+
for ax in fig._localaxes.as_list():
212210
if not hasattr(ax, 'get_subplotspec') or not ax.get_in_layout():
213211
continue
214212

@@ -223,7 +221,6 @@ def _make_layout_margins(fig, renderer, *, w_pad=0, h_pad=0,
223221
hspace=hspace, wspace=wspace)
224222
margin0 = margin.copy()
225223
pos, bbox = _get_pos_and_bbox(ax, renderer)
226-
227224
# the margin is the distance between the bounding box of the axes
228225
# and its position (plus the padding from above)
229226
margin['left'] += pos.x0 - bbox.x0
@@ -281,11 +278,15 @@ def _make_margin_suptitles(fig, renderer, *, w_pad=0, h_pad=0):
281278
# top level figure margin larger.
282279
for panel in fig.panels:
283280
_make_margin_suptitles(panel, renderer, w_pad=w_pad, h_pad=h_pad)
284-
invTransFig = fig.transPanel.inverted().transform_bbox
285-
286-
w_pad, h_pad = (fig.transPanel - fig.transFigure).transform((w_pad, h_pad))
287281

288282
if fig._suptitle is not None and fig._suptitle.get_in_layout():
283+
invTransFig = fig.transSubfigure.inverted().transform_bbox
284+
parenttrans = fig.transFigure
285+
w_pad, h_pad = (fig.transSubfigure -
286+
parenttrans).transform((w_pad, 1 - h_pad))
287+
w_pad, one = (fig.transSubfigure -
288+
parenttrans).transform((w_pad, 1))
289+
h_pad = one - h_pad
289290
bbox = invTransFig(fig._suptitle.get_tightbbox(renderer))
290291
p = fig._suptitle.get_position()
291292
fig._suptitle.set_position((p[0], 1-h_pad))
@@ -429,7 +430,7 @@ def _get_pos_and_bbox(ax, renderer):
429430
fig = ax.figure
430431
pos = ax.get_position(original=True)
431432
# pos is in panel co-ords, but we need in figure for the layout
432-
pos = pos.transformed(fig.transPanel - fig.transFigure)
433+
pos = pos.transformed(fig.transSubfigure - fig.transFigure)
433434
try:
434435
tightbbox = ax.get_tightbbox(renderer=renderer, for_layout_only=True)
435436
except TypeError:
@@ -446,7 +447,7 @@ def _reposition_axes(fig, renderer, *, w_pad=0, h_pad=0, hspace=0, wspace=0):
446447
"""
447448
Reposition all the axes based on the new inner bounding box.
448449
"""
449-
trans_fig_to_panel = fig.transFigure - fig.transPanel
450+
trans_fig_to_panel = fig.transFigure - fig.transSubfigure
450451
for panel in fig.panels:
451452
bbox = panel._layoutgrid.get_outer_bbox()
452453
panel._redo_transform_rel_fig(
@@ -455,9 +456,7 @@ def _reposition_axes(fig, renderer, *, w_pad=0, h_pad=0, hspace=0, wspace=0):
455456
w_pad=w_pad, h_pad=h_pad,
456457
wspace=wspace, hspace=hspace)
457458

458-
# for ax in fig._localaxes:
459-
# if not hasattr(a, 'get_subplotspec'):
460-
for ax in fig.get_axes():
459+
for ax in fig._localaxes.as_list():
461460
if not hasattr(ax, 'get_subplotspec') or not ax.get_in_layout():
462461
continue
463462

@@ -512,7 +511,7 @@ def _reposition_colorbar(cbax, renderer, *, offset=None):
512511
gs = parents[0].get_gridspec()
513512
ncols, nrows = gs.ncols, gs.nrows
514513
fig = cbax.figure
515-
trans_fig_to_panel = fig.transFigure - fig.transPanel
514+
trans_fig_to_panel = fig.transFigure - fig.transSubfigure
516515

517516
cb_rspans, cb_cspans = _get_cb_parent_spans(cbax)
518517
bboxparent = gs._layoutgrid.get_bbox_for_cb(rows=cb_rspans, cols=cb_cspans)
@@ -563,7 +562,7 @@ def _reposition_colorbar(cbax, renderer, *, offset=None):
563562
pbcb = pbcb.translated(0, dy)
564563

565564
pbcb = trans_fig_to_panel.transform_bbox(pbcb)
566-
cbax.set_transform(fig.transPanel)
565+
cbax.set_transform(fig.transSubfigure)
567566
cbax._set_position(pbcb)
568567
cbax.set_aspect(aspect, anchor=anchor, adjustable='box')
569568
return offset

lib/matplotlib/axes/_axes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,7 @@ def indicate_inset(self, bounds, inset_ax=None, *, transform=None,
449449

450450
# decide which two of the lines to keep visible....
451451
pos = inset_ax.get_position()
452-
bboxins = pos.transformed(self.figure.transFigure)
452+
bboxins = pos.transformed(self.figure.transSubfigure)
453453
rectbbox = mtransforms.Bbox.from_bounds(
454454
*bounds
455455
).transformed(transform)

lib/matplotlib/axes/_base.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -673,7 +673,7 @@ def set_figure(self, fig):
673673
super().set_figure(fig)
674674

675675
self.bbox = mtransforms.TransformedBbox(self._position,
676-
fig.transFigure)
676+
fig.transSubfigure)
677677
# these will be updated later as data is added
678678
self.dataLim = mtransforms.Bbox.null()
679679
self._viewLim = mtransforms.Bbox.unit()
@@ -1652,8 +1652,10 @@ def apply_aspect(self, position=None):
16521652
self._set_position(position, which='active')
16531653
return
16541654

1655-
fig_width, fig_height = self.get_figure().get_size_inches()
1656-
fig_aspect = fig_height / fig_width
1655+
trans = self.get_figure().transSubfigure
1656+
bb = mtransforms.Bbox.from_bounds(0, 0, 1, 1).transformed(trans)
1657+
# this is the physical aspect of the panel (or figure):
1658+
fig_aspect = bb.height / bb.width
16571659

16581660
if self._adjustable == 'box':
16591661
if self in self._twinned_axes:

0 commit comments

Comments
 (0)