Skip to content

Add support for (sub-) panel labels to Axes #15771

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,83 @@ def set_title(self, label, fontdict=None, loc=None, pad=None, *, y=None,
title.update(kwargs)
return title

def get_panellabel(self):
"""
Get the (sub-) panel label text.

This is typically a letter (a, b, c, …) identifying the panel for
reference in a figure legend.

Returns
-------
label : str
The panel label string.

"""
return self.panellabel.get_text()

def set_panellabel(self, label, fontdict=None, h_align='axislabel',
v_align='title', pad=0., **kwargs):
"""
Set a (sub-) panel label.

This is typically a letter (a, b, c, …) identifying the panel for
reference in a figure legend.

Parameters
----------
label : str
Text to use for the label
fontdict : dict, optional
A dictionary controlling the appearance of the label text,
the default `fontdict` is::

{'fontsize': rcParams['axes.panellabelsize'],
'fontweight' : rcParams['axes.panellabelweight'],
'verticalalignment': 'baseline',
'horizontalalignment': 'left'}

v_align : {'top', 'title', 'axislabel', 'frame'}, optional
Set vertical alignment of the label. If 'top', align to top
of the title. If 'title', align to baseline of the title. If
'axislabel', align to top of the x axis label (only useful if
x axis is on top). If 'frame', align to the frame.
Defaults to 'title'.
h_align : {'axislabel', 'frame'}, optional
Set vertical alignment of the label. If
'axislabel', align to left edge of the y axis label. If 'frame',
align to the frame. Defaults to 'axislabel'.
pad : float or pair of float, optional
The offset of the label from the left and top of the axes, in
points. If only one number is given, it will be used for both
dimensions. Default is 0.

Returns
-------
text : :class:`~matplotlib.text.Annotation`
The matplotlib annotation instance representing the panel label

Other Parameters
----------------
**kwargs : `~matplotlib.text.Text` properties
Other keyword arguments are text properties, see
:class:`~matplotlib.text.Text` for a list of valid text
properties.
"""
default = {
'fontsize': rcParams['axes.panellabelsize'],
'fontweight': rcParams['axes.panellabelweight'],
'verticalalignment': 'baseline',
'horizontalalignment': 'left'}
self.panellabel.set_text(label)
self.panellabel.update(default)
if fontdict is not None:
self.panellabel.update(fontdict)
self.panellabel.update(kwargs)
self._panellabel_align = (h_align, v_align)
self.panellabel.xyann = (pad,)*2 if isinstance(pad, Number) else pad
return self.panellabel

def get_xlabel(self):
"""
Get the xlabel text string.
Expand Down
42 changes: 42 additions & 0 deletions lib/matplotlib/axes/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,13 @@ def cla(self):
for _title in (self.title, self._left_title, self._right_title):
self._set_artist_props(_title)

self.panellabel = mtext.Annotation(
'', (0.0, 1.0), (0.0, 0.0), xycoords=self._get_panellabel_bbox,
textcoords='offset points'
)
self._panellabel_align = ('axislabel', 'title')
self._set_artist_props(self.panellabel)

# The patch draws the background of the axes. We want this to be below
# the other artists. We use the frame to draw the edges so we are
# setting the edgecolor to None.
Expand Down Expand Up @@ -2661,6 +2668,38 @@ def _update_title_position(self, renderer):
x, _ = title.get_position()
title.set_position((x, ymax))

def _get_panellabel_bbox(self, renderer):
"""
Update bounding box for panel label based title and axis label pos.
"""
bbox = self.get_window_extent(renderer).frozen()
include_xy = []

for axx in self.figure._align_panellabel_x_grp.get_siblings(self):
if self._panellabel_align[0] == 'frame':
xmin = axx.get_window_extent(renderer).xmin
else:
xmin = (axx.yaxis.get_tightbbox(renderer) or
axx.get_window_extent(renderer)).xmin
include_xy.append([xmin, bbox.ymin])

for axy in self.figure._align_panellabel_y_grp.get_siblings(self):
if self._panellabel_align[1] == 'frame':
ymax = axy.get_window_extent(renderer).ymax
elif self._panellabel_align[1] == 'axislabel':
ymax = (axy.xaxis.get_tightbbox(renderer) or
axy.get_window_extent(renderer)).ymax
elif self._panellabel_align[1] == 'top':
ymax = axy.title.get_tightbbox(renderer).ymax
else:
t = axy.title
tpos = t.get_position()
ymax = t.get_transform().transform(tpos)[1]
include_xy.append([bbox.xmin, ymax])

bbox.update_from_data_xy(include_xy, ignore=False)
return bbox

# Drawing
@martist.allow_rasterization
@cbook._delete_parameter(
Expand Down Expand Up @@ -2714,6 +2753,7 @@ def draw(self, renderer=None, inframe=False):
artists.remove(self.title)
artists.remove(self._left_title)
artists.remove(self._right_title)
artists.remove(self.panellabel)

if not self.figure.canvas.is_saving():
artists = [a for a in artists
Expand Down Expand Up @@ -2747,6 +2787,7 @@ def draw(self, renderer=None, inframe=False):
mimage._draw_list_compositing_images(renderer, self, artists)

renderer.close_group('axes')

self.stale = False

def draw_artist(self, a):
Expand Down Expand Up @@ -4045,6 +4086,7 @@ def get_children(self):
*self.child_axes,
*([self.legend_] if self.legend_ is not None else []),
self.patch,
self.panellabel,
]

def contains(self, mouseevent):
Expand Down
57 changes: 57 additions & 0 deletions lib/matplotlib/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,9 @@ def __init__(self,
self._align_xlabel_grp = cbook.Grouper()
self._align_ylabel_grp = cbook.Grouper()

self._align_panellabel_x_grp = cbook.Grouper()
self._align_panellabel_y_grp = cbook.Grouper()

# list of child gridspecs for this figure
self._gridspecs = []

Expand Down Expand Up @@ -2569,6 +2572,60 @@ def align_labels(self, axs=None):
self.align_xlabels(axs=axs)
self.align_ylabels(axs=axs)

def align_panellabels(self, axs=None):
"""
Align the panel labels of subplots 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.

Labels in the same column are moved horizontally to the position of
the left-most label. Labels an the same row are moved vertically to
the position of the top-most label.

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.

Notes
-----
This assumes that ``axs`` are from the same `.GridSpec`, so that
their `.SubplotSpec` positions correspond to figure positions.

Examples
--------
Example with missing y-label::

fig, axs = plt.subplots(2, 1)
axs[0].set_ylabel('YLabel')
axs[0].set_panellabel("a")
axs[1].set_panellabel("b")
fig.align_panellabels()

"""
if axs is None:
axs = self.axes
axs = np.asarray(axs).ravel()
for ax in axs:
ss = ax.get_subplotspec()
row0 = ss.rowspan.start
col0 = ss.colspan.start
# loop through other axes and search ones that share the
# appropriate column or row number.
# Add to a list associated with each axes of siblings.
# This list used in `Axes._get_panellabel_bbox`.
for axc in axs:
if axc is ax:
continue
ssc = axc.get_subplotspec()
if ssc.colspan.start == col0:
self._align_panellabel_x_grp.join(ax, axc)
if ssc.rowspan.start == row0:
self._align_panellabel_y_grp.join(ax, axc)

def add_gridspec(self, nrows=1, ncols=1, **kwargs):
"""
Return a `.GridSpec` that has this figure as a parent. This allows
Expand Down
2 changes: 2 additions & 0 deletions lib/matplotlib/rcsetup.py
Original file line number Diff line number Diff line change
Expand Up @@ -1234,6 +1234,8 @@ def _convert_validator_spec(key, conv):
'axes.titlecolor': ['auto', validate_color_or_auto], # font color of axes title
'axes.titley': [None, validate_float_or_None], # title location, axes units, None means auto
'axes.titlepad': [6.0, validate_float], # pad from axes top decoration to title in points
'axes.panellabelsize': ['x-large', validate_fontsize], # fontsize of the panel label
'axes.panellabelweight': ['bold', validate_fontweight], # font weight of panel label
'axes.grid': [False, validate_bool], # display grid or not
'axes.grid.which': ['major', ['minor', 'both', 'major']], # set whether the grid is drawn on
# 'major' 'minor' or 'both' ticks
Expand Down