[英]Row and column headers in matplotlib's subplots
What's the best practise to add a row and a column header to a grid of subplots generated in a loop in matplotlib
?将一行和一列 header 添加到
matplotlib
的循环中生成的子图网格的最佳实践是什么? I can think of a couple, but not particularly neat:我能想到一对,但不是特别整洁:
set_title()
for the first row only.set_title()
用于第一行。 For rows this doesn't work.text
outside of the plots.text
。 Can you suggest a better alternative?你能推荐一个更好的选择吗?
There are several ways to do this. 有几种方法可以做到这一点。 The easy way is to exploit the y-labels and titles of the plot and then use
fig.tight_layout()
to make room for the labels. 简单的方法是利用绘图的y标签和标题,然后使用
fig.tight_layout()
为标签腾出空间。 Alternatively, you can place additional text in the right location with annotate
and then make room for it semi-manually. 或者,您可以使用
annotate
在正确的位置放置其他文本,然后半手动为其腾出空间。
If you don't have y-labels on your axes, it's easy to exploit the title and y-label of the first row and column of axes. 如果您的轴上没有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()
If you do have y-labels, or if you prefer a bit more flexibility, you can use annotate
to place the labels. 如果您有y标签,或者您更喜欢灵活性,则可以使用
annotate
来放置标签。 This is more complicated, but allows you to have individual plot titles, ylabels, etc in addition to the row and column labels. 这更复杂,但除了行和列标签之外,还允许您拥有单独的绘图标题,ylabels等。
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()
Based on Joe Kington's answer , I put up a function that can be reused across a code base:根据 Joe Kington 的回答,我提出了一个 function 可以在代码库中重用:
It accepts as arguments:它接受为 arguments:
fig
: The figure which contains the axes to work on fig
: 包含要处理的轴的图row_headers
, col_headers
: a sequence of strings to be headers row_headers
, col_headers
:作为标题的字符串序列row_pad
, col_pad
: int
value to adjust padding row_pad
, col_pad
: 调整填充的int
值rotate_row_headers
: whether to rotate by 90° the row headers rotate_row_headers
: 是否将行标题旋转 90°**text_kwargs
: forwarded to ax.annotate(...)
**text_kwargs
: 转发到ax.annotate(...)
Function here, examples below: Function 在这里,示例如下:
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,
)
Here is an example of using it using on a standard grid (no axes spans multiple rows / cols):这是在标准网格上使用它的示例(没有轴跨越多行/列):
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()
If some axes spans multiple rows / cols, it gets a bit less straightforward to assign rows / cols headers correctly.如果某些轴跨越多个行/列,则正确分配行/列标题会变得不那么简单。 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:我没有设法从 function 内部对其进行分类,但要小心给定的
row_headers
和col_headers
arguments 足以使其轻松工作:
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()
The above answer works. 以上答案有效。 Just not that in the second version of the answer, you have:
只是不是在答案的第二个版本中,你有:
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')
instead of: 代替:
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')
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.