Skip to content

Commit

Permalink
Merge pull request #17524 from jklymak/enh-add-suplabels
Browse files Browse the repository at this point in the history
ENH: add supxlabel and supylabel
  • Loading branch information
QuLogic committed Dec 3, 2020
2 parents d1dad03 + 271832c commit ff15ca9
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 49 deletions.
19 changes: 19 additions & 0 deletions doc/users/next_whats_new/suplabels.rst
@@ -0,0 +1,19 @@
supxlabel and supylabel
-----------------------

It is possible to add x- and y-labels to a whole figure, analogous to
`.FigureBase.suptitle` using the new `.FigureBase.supxlabel` and
`.FigureBase.supylabel` methods.

.. plot::

np.random.seed(19680801)
fig, axs = plt.subplots(3, 2, figsize=(5, 5), constrained_layout=True,
sharex=True, sharey=True)

for nn, ax in enumerate(axs.flat):
ax.set_title(f'Channel {nn}')
ax.plot(np.cumsum(np.random.randn(50)))

fig.supxlabel('Time [s]')
fig.supylabel('Data [V]')
47 changes: 42 additions & 5 deletions examples/subplots_axes_and_figures/figure_title.py
@@ -1,12 +1,18 @@
"""
============
Figure title
============
=============================================
Figure labels: suptitle, supxlabel, supylabel
=============================================
Each subplot can have its own title (`.Axes.set_title`). Additionally,
`.Figure.suptitle` adds a centered title at the top of the figure.
Each axes can have a title (or actually three - one each with *loc* "left",
"center", and "right"), but is sometimes desirable to give a whole figure
(or `.SubFigure`) an overall title, using `.FigureBase.suptitle`.
We can also add figure-level x- and y-labels using `.FigureBase.supxlabel` and
`.FigureBase.supylabel`.
"""
from matplotlib.cbook import get_sample_data
import matplotlib.pyplot as plt

import numpy as np


Expand All @@ -24,4 +30,35 @@

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

##############################################################################
# A global x- or y-label can be set using the `.FigureBase.supxlabel` and
# `.FigureBase.supylabel` methods.

fig, axs = plt.subplots(3, 5, figsize=(8, 5), constrained_layout=True,
sharex=True, sharey=True)

fname = get_sample_data('percent_bachelors_degrees_women_usa.csv',
asfileobj=False)
gender_degree_data = np.genfromtxt(fname, delimiter=',', names=True)

majors = ['Health Professions', 'Public Administration', 'Education',
'Psychology', 'Foreign Languages', 'English',
'Art and Performance', 'Biology',
'Agriculture', 'Business',
'Math and Statistics', 'Architecture', 'Physical Sciences',
'Computer Science', 'Engineering']

for nn, ax in enumerate(axs.flat):
ax.set_xlim(1969.5, 2011.1)
column = majors[nn]
column_rec_name = column.replace('\n', '_').replace(' ', '_')

line, = ax.plot('Year', column_rec_name, data=gender_degree_data,
lw=2.5)
ax.set_title(column, fontsize='small', loc='left')
ax.set_ylim([0, 100])
ax.grid()
fig.supxlabel('Year')
fig.supylabel('Percent Degrees Awarded To Women')

plt.show()
35 changes: 25 additions & 10 deletions lib/matplotlib/_constrained_layout.py
Expand Up @@ -18,6 +18,7 @@
import numpy as np

import matplotlib.cbook as cbook
import matplotlib.transforms as mtransforms

_log = logging.getLogger(__name__)

Expand Down Expand Up @@ -276,21 +277,35 @@ 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.

inv_trans_fig = fig.transFigure.inverted().transform_bbox
# get the h_pad and w_pad as distances in the local subfigure coordinates:
padbox = mtransforms.Bbox([[0, 0], [w_pad, h_pad]])
padbox = (fig.transFigure -
fig.transSubfigure).transform_bbox(padbox)
h_pad_local = padbox.height
w_pad_local = padbox.width

for panel in fig.subfigs:
_make_margin_suptitles(panel, renderer, w_pad=w_pad, h_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))
fig._layoutgrid.edit_margin_min('top', bbox.height + 2 * h_pad)
fig._suptitle.set_position((p[0], 1 - h_pad_local))
bbox = inv_trans_fig(fig._suptitle.get_tightbbox(renderer))
fig._layoutgrid.edit_margin_min('top', bbox.height + 2.0 * h_pad)

if fig._supxlabel is not None and fig._supxlabel.get_in_layout():
p = fig._supxlabel.get_position()
fig._supxlabel.set_position((p[0], h_pad_local))
bbox = inv_trans_fig(fig._supxlabel.get_tightbbox(renderer))
fig._layoutgrid.edit_margin_min('bottom', bbox.height + 2.0 * h_pad)

if fig._supylabel is not None and fig._supxlabel.get_in_layout():
p = fig._supylabel.get_position()
fig._supylabel.set_position((w_pad_local, p[1]))
bbox = inv_trans_fig(fig._supylabel.get_tightbbox(renderer))
fig._layoutgrid.edit_margin_min('left', bbox.width + 2.0 * w_pad)


def _match_submerged_margins(fig):
Expand Down
86 changes: 59 additions & 27 deletions lib/matplotlib/figure.py
Expand Up @@ -233,6 +233,8 @@ def __init__(self):
del self._axes

self._suptitle = None
self._supxlabel = None
self._supylabel = None

# constrained_layout:
self._layoutgrid = None
Expand All @@ -254,7 +256,6 @@ def __init__(self):
self.images = []
self.legends = []
self.subfigs = []
self._suptitle = None
self.stale = True
self.suppressComposite = None

Expand Down Expand Up @@ -369,26 +370,26 @@ def get_window_extent(self, *args, **kwargs):
"""
return self.bbox

def suptitle(self, t, **kwargs):
def _suplabels(self, t, info, **kwargs):
"""
Add a centered title to the figure.
Add a centered {name} to the figure.
Parameters
----------
t : str
The title text.
The {name} text.
x : float, default: 0.5
x : float, default: {x0}
The x location of the text in figure coordinates.
y : float, default: 0.98
y : float, default: {y0}
The y location of the text in figure coordinates.
horizontalalignment, ha : {'center', 'left', right'}, default: 'center'
horizontalalignment, ha : {{'center', 'left', 'right'}}, default: {ha}
The horizontal alignment of the text relative to (*x*, *y*).
verticalalignment, va : {'top', 'center', 'bottom', 'baseline'}, \
default: 'top'
verticalalignment, va : {{'top', 'center', 'bottom', 'baseline'}}, \
default: {va}
The vertical alignment of the text relative to (*x*, *y*).
fontsize, size : default: :rc:`figure.titlesize`
Expand All @@ -401,8 +402,8 @@ def suptitle(self, t, **kwargs):
Returns
-------
`.Text`
The instance of the title.
text
The `.Text` instance of the {name}.
Other Parameters
----------------
Expand All @@ -415,19 +416,20 @@ def suptitle(self, t, **kwargs):
**kwargs
Additional kwargs are `matplotlib.text.Text` properties.
Examples
--------
>>> fig.suptitle('This is the figure title', fontsize=12)
"""

manual_position = ('x' in kwargs or 'y' in kwargs)
suplab = getattr(self, info['name'])

x = kwargs.pop('x', 0.5)
y = kwargs.pop('y', 0.98)
x = kwargs.pop('x', info['x0'])
y = kwargs.pop('y', info['y0'])

if 'horizontalalignment' not in kwargs and 'ha' not in kwargs:
kwargs['horizontalalignment'] = 'center'
kwargs['horizontalalignment'] = info['ha']
if 'verticalalignment' not in kwargs and 'va' not in kwargs:
kwargs['verticalalignment'] = 'top'
kwargs['verticalalignment'] = info['va']
if 'rotation' not in kwargs:
kwargs['rotation'] = info['rotation']

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

sup = self.text(x, y, t, **kwargs)
if self._suptitle is not None:
self._suptitle.set_text(t)
self._suptitle.set_position((x, y))
self._suptitle.update_from(sup)
if suplab is not None:
suplab.set_text(t)
suplab.set_position((x, y))
suplab.update_from(sup)
sup.remove()
else:
self._suptitle = sup

suplab = sup
if manual_position:
self._suptitle.set_in_layout(False)

suplab.set_in_layout(False)
setattr(self, info['name'], suplab)
self.stale = True
return self._suptitle
return suplab

@docstring.Substitution(x0=0.5, y0=0.98, name='suptitle', ha='center',
va='top')
@docstring.copy(_suplabels)
def suptitle(self, t, **kwargs):
# docstring from _suplabels...
info = {'name': '_suptitle', 'x0': 0.5, 'y0': 0.98,
'ha': 'center', 'va': 'top', 'rotation': 0}
return self._suplabels(t, info, **kwargs)

@docstring.Substitution(x0=0.5, y0=0.01, name='supxlabel', ha='center',
va='bottom')
@docstring.copy(_suplabels)
def supxlabel(self, t, **kwargs):
# docstring from _suplabels...
info = {'name': '_supxlabel', 'x0': 0.5, 'y0': 0.01,
'ha': 'center', 'va': 'bottom', 'rotation': 0}
return self._suplabels(t, info, **kwargs)

@docstring.Substitution(x0=0.02, y0=0.5, name='supylabel', ha='left',
va='center')
@docstring.copy(_suplabels)
def supylabel(self, t, **kwargs):
# docstring from _suplabels...
info = {'name': '_supylabel', 'x0': 0.02, 'y0': 0.5,
'ha': 'left', 'va': 'center', 'rotation': 'vertical',
'rotation_mode': 'anchor'}
return self._suplabels(t, info, **kwargs)

def get_edgecolor(self):
"""Get the edge color of the Figure rectangle."""
Expand Down Expand Up @@ -2814,6 +2843,9 @@ def clf(self, keep_observers=False):
if not keep_observers:
self._axobservers = cbook.CallbackRegistry()
self._suptitle = None
self._supxlabel = None
self._supylabel = None

if self.get_constrained_layout():
self.init_layoutgrid()
self.stale = True
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 45 additions & 2 deletions lib/matplotlib/tests/test_figure.py
Expand Up @@ -838,7 +838,7 @@ def test_reused_gridspec():
savefig_kwarg={'facecolor': 'teal'},
remove_text=False)
def test_subfigure():
np.random.seed(19680808)
np.random.seed(19680801)
fig = plt.figure(constrained_layout=True)
sub = fig.subfigures(1, 2)

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

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

fig.suptitle('Figure suptitle', fontsize='xx-large')


@image_comparison(['test_subfigure_double.png'], style='mpl20',
savefig_kwarg={'facecolor': 'teal'},
remove_text=False)
def test_subfigure_double():
# test assigning the subfigure via subplotspec
np.random.seed(19680801)

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 ax in axsnest0:
fontsize = 12
pc = ax.pcolormesh(np.random.randn(30, 30), vmin=-2.5, vmax=2.5)
ax.set_xlabel('x-label', fontsize=fontsize)
ax.set_ylabel('y-label', fontsize=fontsize)
ax.set_title('Title', fontsize=fontsize)

subfigsnest[0].colorbar(pc, ax=axsnest0)

subfigsnest[1].suptitle('subfigsnest[1]')
subfigsnest[1].set_facecolor('g')
axsnest1 = subfigsnest[1].subplots(3, 1, sharex=True)
for nn, ax in enumerate(axsnest1):
ax.set_ylabel(f'ylabel{nn}')
subfigsnest[1].supxlabel('supxlabel')
subfigsnest[1].supylabel('supylabel')

axsRight = subfigs[1].subplots(2, 2)
20 changes: 15 additions & 5 deletions lib/matplotlib/tight_layout.py
Expand Up @@ -108,20 +108,30 @@ def auto_adjust_subplotpars(
if not margin_left:
margin_left = (max(hspaces[:, 0].max(), 0)
+ pad_inches / fig_width_inch)
suplabel = fig._supylabel
if suplabel and suplabel.get_in_layout():
rel_width = fig.transFigure.inverted().transform_bbox(
suplabel.get_window_extent(renderer)).width
margin_left += rel_width + pad_inches / fig_width_inch

if not margin_right:
margin_right = (max(hspaces[:, -1].max(), 0)
+ pad_inches / fig_width_inch)
if not margin_top:
margin_top = (max(vspaces[0, :].max(), 0)
+ pad_inches / fig_height_inch)
suptitle = fig._suptitle
if suptitle and suptitle.get_in_layout():
rel_suptitle_height = fig.transFigure.inverted().transform_bbox(
suptitle.get_window_extent(renderer)).height
margin_top += rel_suptitle_height + pad_inches / fig_height_inch
if fig._suptitle and fig._suptitle.get_in_layout():
rel_height = fig.transFigure.inverted().transform_bbox(
fig._suptitle.get_window_extent(renderer)).height
margin_top += rel_height + pad_inches / fig_height_inch
if not margin_bottom:
margin_bottom = (max(vspaces[-1, :].max(), 0)
+ pad_inches / fig_height_inch)
suplabel = fig._supxlabel
if suplabel and suplabel.get_in_layout():
rel_height = fig.transFigure.inverted().transform_bbox(
suplabel.get_window_extent(renderer)).height
margin_bottom += rel_height + pad_inches / fig_height_inch

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

0 comments on commit ff15ca9

Please sign in to comment.