Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit ff15ca9

Browse filesBrowse files
authored
Merge pull request #17524 from jklymak/enh-add-suplabels
ENH: add supxlabel and supylabel
2 parents d1dad03 + 271832c commit ff15ca9
Copy full SHA for ff15ca9

File tree

Expand file treeCollapse file tree

9 files changed

+205
-49
lines changed
Filter options
Expand file treeCollapse file tree

9 files changed

+205
-49
lines changed
+19Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
supxlabel and supylabel
2+
-----------------------
3+
4+
It is possible to add x- and y-labels to a whole figure, analogous to
5+
`.FigureBase.suptitle` using the new `.FigureBase.supxlabel` and
6+
`.FigureBase.supylabel` methods.
7+
8+
.. plot::
9+
10+
np.random.seed(19680801)
11+
fig, axs = plt.subplots(3, 2, figsize=(5, 5), constrained_layout=True,
12+
sharex=True, sharey=True)
13+
14+
for nn, ax in enumerate(axs.flat):
15+
ax.set_title(f'Channel {nn}')
16+
ax.plot(np.cumsum(np.random.randn(50)))
17+
18+
fig.supxlabel('Time [s]')
19+
fig.supylabel('Data [V]')

‎examples/subplots_axes_and_figures/figure_title.py

Copy file name to clipboardExpand all lines: examples/subplots_axes_and_figures/figure_title.py
+42-5Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
"""
2-
============
3-
Figure title
4-
============
2+
=============================================
3+
Figure labels: suptitle, supxlabel, supylabel
4+
=============================================
55
6-
Each subplot can have its own title (`.Axes.set_title`). Additionally,
7-
`.Figure.suptitle` adds a centered title at the top of the figure.
6+
Each axes can have a title (or actually three - one each with *loc* "left",
7+
"center", and "right"), but is sometimes desirable to give a whole figure
8+
(or `.SubFigure`) an overall title, using `.FigureBase.suptitle`.
9+
10+
We can also add figure-level x- and y-labels using `.FigureBase.supxlabel` and
11+
`.FigureBase.supylabel`.
812
"""
13+
from matplotlib.cbook import get_sample_data
914
import matplotlib.pyplot as plt
15+
1016
import numpy as np
1117

1218

@@ -24,4 +30,35 @@
2430

2531
fig.suptitle('Different types of oscillations', fontsize=16)
2632

33+
##############################################################################
34+
# A global x- or y-label can be set using the `.FigureBase.supxlabel` and
35+
# `.FigureBase.supylabel` methods.
36+
37+
fig, axs = plt.subplots(3, 5, figsize=(8, 5), constrained_layout=True,
38+
sharex=True, sharey=True)
39+
40+
fname = get_sample_data('percent_bachelors_degrees_women_usa.csv',
41+
asfileobj=False)
42+
gender_degree_data = np.genfromtxt(fname, delimiter=',', names=True)
43+
44+
majors = ['Health Professions', 'Public Administration', 'Education',
45+
'Psychology', 'Foreign Languages', 'English',
46+
'Art and Performance', 'Biology',
47+
'Agriculture', 'Business',
48+
'Math and Statistics', 'Architecture', 'Physical Sciences',
49+
'Computer Science', 'Engineering']
50+
51+
for nn, ax in enumerate(axs.flat):
52+
ax.set_xlim(1969.5, 2011.1)
53+
column = majors[nn]
54+
column_rec_name = column.replace('\n', '_').replace(' ', '_')
55+
56+
line, = ax.plot('Year', column_rec_name, data=gender_degree_data,
57+
lw=2.5)
58+
ax.set_title(column, fontsize='small', loc='left')
59+
ax.set_ylim([0, 100])
60+
ax.grid()
61+
fig.supxlabel('Year')
62+
fig.supylabel('Percent Degrees Awarded To Women')
63+
2764
plt.show()

‎lib/matplotlib/_constrained_layout.py

Copy file name to clipboardExpand all lines: lib/matplotlib/_constrained_layout.py
+25-10Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import numpy as np
1919

2020
import matplotlib.cbook as cbook
21+
import matplotlib.transforms as mtransforms
2122

2223
_log = logging.getLogger(__name__)
2324

@@ -276,21 +277,35 @@ def _make_layout_margins(fig, renderer, *, w_pad=0, h_pad=0,
276277
def _make_margin_suptitles(fig, renderer, *, w_pad=0, h_pad=0):
277278
# Figure out how large the suptitle is and make the
278279
# top level figure margin larger.
280+
281+
inv_trans_fig = fig.transFigure.inverted().transform_bbox
282+
# get the h_pad and w_pad as distances in the local subfigure coordinates:
283+
padbox = mtransforms.Bbox([[0, 0], [w_pad, h_pad]])
284+
padbox = (fig.transFigure -
285+
fig.transSubfigure).transform_bbox(padbox)
286+
h_pad_local = padbox.height
287+
w_pad_local = padbox.width
288+
279289
for panel in fig.subfigs:
280290
_make_margin_suptitles(panel, renderer, w_pad=w_pad, h_pad=h_pad)
281291

282292
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
290-
bbox = invTransFig(fig._suptitle.get_tightbbox(renderer))
291293
p = fig._suptitle.get_position()
292-
fig._suptitle.set_position((p[0], 1-h_pad))
293-
fig._layoutgrid.edit_margin_min('top', bbox.height + 2 * h_pad)
294+
fig._suptitle.set_position((p[0], 1 - h_pad_local))
295+
bbox = inv_trans_fig(fig._suptitle.get_tightbbox(renderer))
296+
fig._layoutgrid.edit_margin_min('top', bbox.height + 2.0 * h_pad)
297+
298+
if fig._supxlabel is not None and fig._supxlabel.get_in_layout():
299+
p = fig._supxlabel.get_position()
300+
fig._supxlabel.set_position((p[0], h_pad_local))
301+
bbox = inv_trans_fig(fig._supxlabel.get_tightbbox(renderer))
302+
fig._layoutgrid.edit_margin_min('bottom', bbox.height + 2.0 * h_pad)
303+
304+
if fig._supylabel is not None and fig._supxlabel.get_in_layout():
305+
p = fig._supylabel.get_position()
306+
fig._supylabel.set_position((w_pad_local, p[1]))
307+
bbox = inv_trans_fig(fig._supylabel.get_tightbbox(renderer))
308+
fig._layoutgrid.edit_margin_min('left', bbox.width + 2.0 * w_pad)
294309

295310

296311
def _match_submerged_margins(fig):

‎lib/matplotlib/figure.py

Copy file name to clipboardExpand all lines: lib/matplotlib/figure.py
+59-27Lines changed: 59 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,8 @@ def __init__(self):
233233
del self._axes
234234

235235
self._suptitle = None
236+
self._supxlabel = None
237+
self._supylabel = None
236238

237239
# constrained_layout:
238240
self._layoutgrid = None
@@ -254,7 +256,6 @@ def __init__(self):
254256
self.images = []
255257
self.legends = []
256258
self.subfigs = []
257-
self._suptitle = None
258259
self.stale = True
259260
self.suppressComposite = None
260261

@@ -369,26 +370,26 @@ def get_window_extent(self, *args, **kwargs):
369370
"""
370371
return self.bbox
371372

372-
def suptitle(self, t, **kwargs):
373+
def _suplabels(self, t, info, **kwargs):
373374
"""
374-
Add a centered title to the figure.
375+
Add a centered {name} to the figure.
375376
376377
Parameters
377378
----------
378379
t : str
379-
The title text.
380+
The {name} text.
380381
381-
x : float, default: 0.5
382+
x : float, default: {x0}
382383
The x location of the text in figure coordinates.
383384
384-
y : float, default: 0.98
385+
y : float, default: {y0}
385386
The y location of the text in figure coordinates.
386387
387-
horizontalalignment, ha : {'center', 'left', right'}, default: 'center'
388+
horizontalalignment, ha : {{'center', 'left', 'right'}}, default: {ha}
388389
The horizontal alignment of the text relative to (*x*, *y*).
389390
390-
verticalalignment, va : {'top', 'center', 'bottom', 'baseline'}, \
391-
default: 'top'
391+
verticalalignment, va : {{'top', 'center', 'bottom', 'baseline'}}, \
392+
default: {va}
392393
The vertical alignment of the text relative to (*x*, *y*).
393394
394395
fontsize, size : default: :rc:`figure.titlesize`
@@ -401,8 +402,8 @@ def suptitle(self, t, **kwargs):
401402
402403
Returns
403404
-------
404-
`.Text`
405-
The instance of the title.
405+
text
406+
The `.Text` instance of the {name}.
406407
407408
Other Parameters
408409
----------------
@@ -415,19 +416,20 @@ def suptitle(self, t, **kwargs):
415416
**kwargs
416417
Additional kwargs are `matplotlib.text.Text` properties.
417418
418-
Examples
419-
--------
420-
>>> fig.suptitle('This is the figure title', fontsize=12)
421419
"""
420+
422421
manual_position = ('x' in kwargs or 'y' in kwargs)
422+
suplab = getattr(self, info['name'])
423423

424-
x = kwargs.pop('x', 0.5)
425-
y = kwargs.pop('y', 0.98)
424+
x = kwargs.pop('x', info['x0'])
425+
y = kwargs.pop('y', info['y0'])
426426

427427
if 'horizontalalignment' not in kwargs and 'ha' not in kwargs:
428-
kwargs['horizontalalignment'] = 'center'
428+
kwargs['horizontalalignment'] = info['ha']
429429
if 'verticalalignment' not in kwargs and 'va' not in kwargs:
430-
kwargs['verticalalignment'] = 'top'
430+
kwargs['verticalalignment'] = info['va']
431+
if 'rotation' not in kwargs:
432+
kwargs['rotation'] = info['rotation']
431433

432434
if 'fontproperties' not in kwargs:
433435
if 'fontsize' not in kwargs and 'size' not in kwargs:
@@ -436,19 +438,46 @@ def suptitle(self, t, **kwargs):
436438
kwargs['weight'] = mpl.rcParams['figure.titleweight']
437439

438440
sup = self.text(x, y, t, **kwargs)
439-
if self._suptitle is not None:
440-
self._suptitle.set_text(t)
441-
self._suptitle.set_position((x, y))
442-
self._suptitle.update_from(sup)
441+
if suplab is not None:
442+
suplab.set_text(t)
443+
suplab.set_position((x, y))
444+
suplab.update_from(sup)
443445
sup.remove()
444446
else:
445-
self._suptitle = sup
446-
447+
suplab = sup
447448
if manual_position:
448-
self._suptitle.set_in_layout(False)
449-
449+
suplab.set_in_layout(False)
450+
setattr(self, info['name'], suplab)
450451
self.stale = True
451-
return self._suptitle
452+
return suplab
453+
454+
@docstring.Substitution(x0=0.5, y0=0.98, name='suptitle', ha='center',
455+
va='top')
456+
@docstring.copy(_suplabels)
457+
def suptitle(self, t, **kwargs):
458+
# docstring from _suplabels...
459+
info = {'name': '_suptitle', 'x0': 0.5, 'y0': 0.98,
460+
'ha': 'center', 'va': 'top', 'rotation': 0}
461+
return self._suplabels(t, info, **kwargs)
462+
463+
@docstring.Substitution(x0=0.5, y0=0.01, name='supxlabel', ha='center',
464+
va='bottom')
465+
@docstring.copy(_suplabels)
466+
def supxlabel(self, t, **kwargs):
467+
# docstring from _suplabels...
468+
info = {'name': '_supxlabel', 'x0': 0.5, 'y0': 0.01,
469+
'ha': 'center', 'va': 'bottom', 'rotation': 0}
470+
return self._suplabels(t, info, **kwargs)
471+
472+
@docstring.Substitution(x0=0.02, y0=0.5, name='supylabel', ha='left',
473+
va='center')
474+
@docstring.copy(_suplabels)
475+
def supylabel(self, t, **kwargs):
476+
# docstring from _suplabels...
477+
info = {'name': '_supylabel', 'x0': 0.02, 'y0': 0.5,
478+
'ha': 'left', 'va': 'center', 'rotation': 'vertical',
479+
'rotation_mode': 'anchor'}
480+
return self._suplabels(t, info, **kwargs)
452481

453482
def get_edgecolor(self):
454483
"""Get the edge color of the Figure rectangle."""
@@ -2814,6 +2843,9 @@ def clf(self, keep_observers=False):
28142843
if not keep_observers:
28152844
self._axobservers = cbook.CallbackRegistry()
28162845
self._suptitle = None
2846+
self._supxlabel = None
2847+
self._supylabel = None
2848+
28172849
if self.get_constrained_layout():
28182850
self.init_layoutgrid()
28192851
self.stale = True
Loading
Loading
Loading

‎lib/matplotlib/tests/test_figure.py

Copy file name to clipboardExpand all lines: lib/matplotlib/tests/test_figure.py
+45-2Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -838,7 +838,7 @@ def test_reused_gridspec():
838838
savefig_kwarg={'facecolor': 'teal'},
839839
remove_text=False)
840840
def test_subfigure():
841-
np.random.seed(19680808)
841+
np.random.seed(19680801)
842842
fig = plt.figure(constrained_layout=True)
843843
sub = fig.subfigures(1, 2)
844844

@@ -862,7 +862,7 @@ def test_subfigure():
862862
remove_text=False)
863863
def test_subfigure_ss():
864864
# test assigning the subfigure via subplotspec
865-
np.random.seed(19680808)
865+
np.random.seed(19680801)
866866
fig = plt.figure(constrained_layout=True)
867867
gs = fig.add_gridspec(1, 2)
868868

@@ -879,3 +879,46 @@ def test_subfigure_ss():
879879
ax.set_title('Axes')
880880

881881
fig.suptitle('Figure suptitle', fontsize='xx-large')
882+
883+
884+
@image_comparison(['test_subfigure_double.png'], style='mpl20',
885+
savefig_kwarg={'facecolor': 'teal'},
886+
remove_text=False)
887+
def test_subfigure_double():
888+
# test assigning the subfigure via subplotspec
889+
np.random.seed(19680801)
890+
891+
fig = plt.figure(constrained_layout=True, figsize=(10, 8))
892+
893+
fig.suptitle('fig')
894+
895+
subfigs = fig.subfigures(1, 2, wspace=0.07)
896+
897+
subfigs[0].set_facecolor('coral')
898+
subfigs[0].suptitle('subfigs[0]')
899+
900+
subfigs[1].set_facecolor('coral')
901+
subfigs[1].suptitle('subfigs[1]')
902+
903+
subfigsnest = subfigs[0].subfigures(2, 1, height_ratios=[1, 1.4])
904+
subfigsnest[0].suptitle('subfigsnest[0]')
905+
subfigsnest[0].set_facecolor('r')
906+
axsnest0 = subfigsnest[0].subplots(1, 2, sharey=True)
907+
for ax in axsnest0:
908+
fontsize = 12
909+
pc = ax.pcolormesh(np.random.randn(30, 30), vmin=-2.5, vmax=2.5)
910+
ax.set_xlabel('x-label', fontsize=fontsize)
911+
ax.set_ylabel('y-label', fontsize=fontsize)
912+
ax.set_title('Title', fontsize=fontsize)
913+
914+
subfigsnest[0].colorbar(pc, ax=axsnest0)
915+
916+
subfigsnest[1].suptitle('subfigsnest[1]')
917+
subfigsnest[1].set_facecolor('g')
918+
axsnest1 = subfigsnest[1].subplots(3, 1, sharex=True)
919+
for nn, ax in enumerate(axsnest1):
920+
ax.set_ylabel(f'ylabel{nn}')
921+
subfigsnest[1].supxlabel('supxlabel')
922+
subfigsnest[1].supylabel('supylabel')
923+
924+
axsRight = subfigs[1].subplots(2, 2)

‎lib/matplotlib/tight_layout.py

Copy file name to clipboardExpand all lines: lib/matplotlib/tight_layout.py
+15-5Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,20 +108,30 @@ def auto_adjust_subplotpars(
108108
if not margin_left:
109109
margin_left = (max(hspaces[:, 0].max(), 0)
110110
+ pad_inches / fig_width_inch)
111+
suplabel = fig._supylabel
112+
if suplabel and suplabel.get_in_layout():
113+
rel_width = fig.transFigure.inverted().transform_bbox(
114+
suplabel.get_window_extent(renderer)).width
115+
margin_left += rel_width + pad_inches / fig_width_inch
116+
111117
if not margin_right:
112118
margin_right = (max(hspaces[:, -1].max(), 0)
113119
+ pad_inches / fig_width_inch)
114120
if not margin_top:
115121
margin_top = (max(vspaces[0, :].max(), 0)
116122
+ pad_inches / fig_height_inch)
117-
suptitle = fig._suptitle
118-
if suptitle and suptitle.get_in_layout():
119-
rel_suptitle_height = fig.transFigure.inverted().transform_bbox(
120-
suptitle.get_window_extent(renderer)).height
121-
margin_top += rel_suptitle_height + pad_inches / fig_height_inch
123+
if fig._suptitle and fig._suptitle.get_in_layout():
124+
rel_height = fig.transFigure.inverted().transform_bbox(
125+
fig._suptitle.get_window_extent(renderer)).height
126+
margin_top += rel_height + pad_inches / fig_height_inch
122127
if not margin_bottom:
123128
margin_bottom = (max(vspaces[-1, :].max(), 0)
124129
+ pad_inches / fig_height_inch)
130+
suplabel = fig._supxlabel
131+
if suplabel and suplabel.get_in_layout():
132+
rel_height = fig.transFigure.inverted().transform_bbox(
133+
suplabel.get_window_extent(renderer)).height
134+
margin_bottom += rel_height + pad_inches / fig_height_inch
125135

126136
if margin_left + margin_right >= 1:
127137
cbook._warn_external('Tight layout not applied. The left and right '

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.