Row and column headers in matplotlib's subplots

matplotlib循环中生成的子图的网格中添加行和列标题的最佳实践是什么?我能想到一对夫妇,但不是特别整洁:

  1. For columns, with a counter to your loop you can use set_title() for the first row only. For rows this doesn't work. You would have to draw text outside of the plots.
  2. 您可以在顶部添加一行额外的次要情节,在左侧添加一列额外的次要情节,并在该次要情节的中间绘制文本。

Can you suggest a better alternative?

enter image description here

78615 次浏览

有几种方法可以做到这一点。简单的方法是利用 Y 标签和情节标题,然后使用 fig.tight_layout()为标签腾出空间。或者,您可以使用 annotate在正确的位置放置额外的文本,然后半手动地为它腾出空间。


如果坐标轴上没有 y 标签,那么很容易利用第一行和第一列坐标轴的标题和 y 标签。

import matplotlib.pyplot as plt


cols = ['Column {}'.format(col) for col in range(1, 4)]
rows = ['Row {}'.format(row) for row in ['A', 'B', 'C', 'D']]


fig, axes = plt.subplots(nrows=4, ncols=3, figsize=(12, 8))


for ax, col in zip(axes[0], cols):
ax.set_title(col)


for ax, row in zip(axes[:,0], rows):
ax.set_ylabel(row, rotation=0, size='large')


fig.tight_layout()
plt.show()

enter image description here


如果确实有 y 标签,或者希望更灵活一些,可以使用 annotate来放置标签。这比较复杂,但是允许除了行标签和列标签之外还有单独的情节标题、 ylabel 等。

import matplotlib.pyplot as plt
from matplotlib.transforms import offset_copy




cols = ['Column {}'.format(col) for col in range(1, 4)]
rows = ['Row {}'.format(row) for row in ['A', 'B', 'C', 'D']]


fig, axes = plt.subplots(nrows=4, ncols=3, figsize=(12, 8))
plt.setp(axes.flat, xlabel='X-label', ylabel='Y-label')


pad = 5 # in points


for ax, col in zip(axes[0], cols):
ax.annotate(col, xy=(0.5, 1), xytext=(0, pad),
xycoords='axes fraction', textcoords='offset points',
size='large', ha='center', va='baseline')


for ax, row in zip(axes[:,0], rows):
ax.annotate(row, xy=(0, 0.5), xytext=(-ax.yaxis.labelpad - pad, 0),
xycoords=ax.yaxis.label, textcoords='offset points',
size='large', ha='right', va='center')


fig.tight_layout()
# tight_layout doesn't take these labels into account. We'll need
# to make some room. These numbers are are manually tweaked.
# You could automatically calculate them, but it's a pain.
fig.subplots_adjust(left=0.15, top=0.95)


plt.show()

enter image description here

上面的答案是可行的,只是在第二个版本的答案中,你有:

for ax, row in zip(axes[:,0], rows):
ax.annotate(col, xy=(0, 0.5), xytext=(-ax.yaxis.labelpad-pad,0),
xycoords=ax.yaxis.label, textcoords='offset points',
size='large', ha='right', va='center')

而不是:

for ax, row in zip(axes[:,0], rows):
ax.annotate(row,xy=(0, 0.5), xytext=(-ax.yaxis.labelpad-pad,0),
xycoords=ax.yaxis.label, textcoords='offset points',
size='large', ha='right', va='center')

基于 Joe 金顿的回答,我创建了一个可以跨代码库重用的函数:

它接受以下理由:

  • fig: 包含要处理的轴的图形
  • row_headerscol_headers: 作为头的字符串序列
  • row_padcol_pad: int值来调整填充
  • rotate_row_headers: 是否按90 ° 行标题旋转
  • 转发到 ax.annotate(...)

这里的函数,例子如下:

import numpy as np


def add_headers(
fig,
*,
row_headers=None,
col_headers=None,
row_pad=1,
col_pad=5,
rotate_row_headers=True,
**text_kwargs
):
# Based on https://stackoverflow.com/a/25814386


axes = fig.get_axes()


for ax in axes:
sbs = ax.get_subplotspec()


# Putting headers on cols
if (col_headers is not None) and sbs.is_first_row():
ax.annotate(
col_headers[sbs.colspan.start],
xy=(0.5, 1),
xytext=(0, col_pad),
xycoords="axes fraction",
textcoords="offset points",
ha="center",
va="baseline",
**text_kwargs,
)


# Putting headers on rows
if (row_headers is not None) and sbs.is_first_col():
ax.annotate(
row_headers[sbs.rowspan.start],
xy=(0, 0.5),
xytext=(-ax.yaxis.labelpad - row_pad, 0),
xycoords=ax.yaxis.label,
textcoords="offset points",
ha="right",
va="center",
rotation=rotate_row_headers * 90,
**text_kwargs,
)

下面是在标准网格上使用它的一个示例(没有轴跨越多个行/协议) :

import random
import matplotlib.pyplot as plt


mosaic = [
["A0", "A1", "A2"],
["B0", "B1", "B2"],
]
row_headers = ["Row A", "Row B"]
col_headers = ["Col 0", "Col 1", "Col 2"]


subplots_kwargs = dict(sharex=True, sharey=True, figsize=(10, 6))
fig, axes = plt.subplot_mosaic(mosaic, **subplots_kwargs)


font_kwargs = dict(fontfamily="monospace", fontweight="bold", fontsize="large")
add_headers(fig, col_headers=col_headers, row_headers=row_headers, **font_kwargs)


plt.show()

result: regular grid

如果一些轴跨越多个行/协议,那么正确分配行/协议标头就不那么简单了。 I didn't managed to sort it out from inside the function, but being careful to the given row_headers and col_headers arguments is enough to make it work easily:

mosaic = [
["A0", "A1", "A1", "A2"],
["A0", "A1", "A1", "A2"],
["B0", "B1", "B1", "B2"],
]


row_headers = ["A", "A", "B"]  # or
row_headers = ["A", None, "B"]  # or
row_headers = {0: "A", 2: "B"}


col_headers = ["0", "1", "1", "2"]  # or
col_headers = ["0", "1", None, "2"]  # or
col_headers = {0: "0", 1: "1", 3: "2"}


fig, axes = plt.subplot_mosaic(mosaic, **subplots_kwargs)
add_headers(fig, col_headers=col_headers, row_headers=row_headers, **font_kwargs)
plt.show()

result: non-regular grid