<think>
我们有一个需求:在Python中使用Matplotlib创建两行一列的子图(即两个子图垂直排列),具体要求如下:
1. 标题放在子图1(上方子图)的上方。
2. 图例放在子图1和子图2(即两个子图)的中间,并且以一行排列(水平排列)。
根据引用内容,我们知道Matplotlib提供了几种布局调整方法,如约束布局(constrained_layout)、紧密布局(tight_layout)和自定义布局。同时,引用中提到了使用subplots函数创建子图,以及使用tight_layout来调整子图间距。
然而,为了满足特定的图例位置要求(在两个子图中间且水平排列),我们可能需要结合使用一些额外的技巧,比如:
- 使用fig.legend()来创建整个图形的图例,而不是单个子图的图例。
- 调整图形区域(包括子图区域和图例区域)的布局。
具体步骤:
1. 创建图形和子图:使用plt.subplots(nrows=2, ncols=1)创建两行一列的子图。
2. 设置整个图形的标题:由于要求标题在子图1的上方,我们可以使用fig.suptitle()设置总标题,或者使用ax1.set_title()设置第一个子图的标题(但这样标题会紧贴第一个子图)。根据需求,标题要放在子图1上方,可能更接近整个图形的顶部,因此使用fig.suptitle()并调整其位置可能更合适,但注意引用[1]提到多个子图紧凑时标题可能重叠,所以我们也要调整标题的位置。
3. 绘制两个子图的内容,并分别设置标签(label)以便图例使用。
4. 创建图例:由于图例要放在两个子图中间,我们可以创建一个属于整个图形的图例(使用fig.legend()),并指定图例的位置(loc)为自定义位置,或者使用预定义的位置字符串,如'center',然后调整其具体位置。
另一种方法是手动调整图例的位置,通过指定图例在图形中的坐标(使用bbox_to_anchor参数)。
但是,注意引用[3]提到约束布局仅适用于调整子图中的刻度标签、轴标签、标题和图例(指子图内部的图例)的位置,而不调整其他元素。因此,如果我们使用fig.legend()创建整个图形的图例,约束布局可能不会自动调整它。
因此,我们可以考虑以下方法:
- 不使用约束布局,而使用tight_layout,然后手动调整图例位置。
- 或者,使用约束布局,然后手动调整图例位置(因为图例在两个子图中间的需求可能无法自动满足)。
由于要求图例在两个子图中间,我们可以:
a. 在第一个子图(ax1)中绘制所有需要图例的线条,并收集标签(但这样图例会出现在ax1内部)。
b. 在第二个子图(ax2)中绘制,同样收集标签。
c. 然后,我们创建一个共同的图例,放在两个子图之间。
我们可以通过以下步骤实现:
1. 在ax1和ax2中绘制图形时,设置label。
2. 通过ax1.get_legend_handles_labels()和ax2.get_legend_handles_labels()获取两个子图中的图例句柄和标签。
3. 合并句柄和标签。
4. 使用fig.legend()创建图例,并指定图例的位置为图形中间偏下的位置。
然后,我们需要调整子图的布局,为图例留出空间。我们可以使用constrained_layout=True,然后调整图例的位置参数,或者使用tight_layout并指定rect参数(调整子图区域的位置)来为图例留出空间。
但是,引用[2]中使用了tight_layout调整子图之间的距离,但我们的图例是在图形中间的,所以我们需要在子图之间留出空间给图例。
具体实现计划:
1. 创建图形和子图,设置constrained_layout=True(或者使用tight_layout,但引用[2]提到tight_layout可以调整子图间距,而引用[1]提到约束布局也能自动调整,这里我们尝试使用constrained_layout,因为引用[3]提到了约束布局)。
2. 设置整个图形的标题(fig.suptitle)放在图形顶部(注意:如果放在子图1上方,可能需要调整suptitle的y坐标,使其位于子图1的上方,但又不至于太靠上)。
3. 在两个子图中绘制图形,并收集所有图例的句柄和标签。
4. 使用fig.legend()创建图例,并设置位置参数(loc)为两个子图中间的位置。我们可以通过指定loc='lower center',然后调整其垂直位置(比如向上移动一些)来达到两个子图中间的效果。或者使用bbox_to_anchor参数(在图形坐标中指定位置)来精确定位。
由于两个子图垂直排列,整个图形可以看作三个部分:顶部标题区域、子图1区域、子图2区域、图例区域(位于子图1和子图2之间)和底部空白。因此,我们可以:
- 将图例放在整个图形高度的中间位置(0.5),但实际上因为有两个子图,所以中间位置应该是子图1底部和子图2顶部之间的中间位置。
但constrained_layout可能不会自动为整个图形的图例留出空间(引用[3]说约束布局仅适用于子图中的元素),所以我们需要手动调整。
因此,另一种方法是:
- 不使用constrained_layout,而使用手动调整子图位置和大小的方法,通过设置GridSpec来实现。
考虑到我们要精确控制图例的位置,我们可以:
- 使用GridSpec来创建子图,并留出图例的空间。
步骤:
1. 创建一个GridSpec布局,将整个图形分成三部分:顶部(标题区域),中间上(子图1),中间下(图例区域),底部(子图2)。具体行数比例可以调整。
2. 在GridSpec的顶部区域设置标题(使用fig.text或fig.suptitle)。
3. 在中间上的区域创建第一个子图ax1。
4. 在中间下的区域(图例区域)我们不创建子图,而是用来放置图例。注意,这个区域的高度应该很小,仅用于放置一行图例。
5. 在底部区域创建第二个子图ax2。
6. 然后,绘制图形,并在ax1和ax2中设置label。
7. 合并所有句柄和标签,然后在中间下的区域(即我们预留的空间)创建图例。
然而,这种方法需要手动调整高度比例,可能较为复杂。
为了简便,我们可以尝试使用constrained_layout,并配合手动调整图例位置。
考虑以下步骤(使用constrained_layout):
1. 创建图形:fig, (ax1, ax2) = plt.subplots(nrows=2, ncols=1, constrained_layout=True)
2. 设置整个图形的标题:fig.suptitle('总标题', y=1.05) # 调整y值使其高于子图1
3. 在ax1和ax2中绘图,并设置label。
4. 获取两个子图的句柄和标签,合并。
5. 使用fig.legend(),并指定位置,例如:
- loc='upper center', bbox_to_anchor=(0.5, 0.5), ncol=图例行数(一行的话ncol为标签个数)
但是,这里的bbox_to_anchor是相对于整个图形(figure)的坐标,其中(0,0)是左下角,(1,1)是右上角。两个子图中间的位置大约是y=0.5左右。但由于constrained_layout会自动调整子图区域,实际中间位置可能不是0.5,我们可以尝试0.5,然后观察效果调整。
6. 但是,使用constrained_layout时,我们添加的图例可能会与子图重叠,因为constrained_layout不会自动考虑fig.legend()。因此,我们可以关闭constrained_layout,改为手动调整布局(使用plt.tight_layout)并设置rect参数来为图例留出空间。
因此,我们决定不使用constrained_layout,而是使用tight_layout,并通过调整子图区域(设置rect)来为图例留出空间。
具体步骤:
1. 创建图形和两个子图(垂直排列):fig, axs = plt.subplots(2, 1, figsize=(6,8))
2. 设置整个图形的标题:fig.suptitle('总标题')
3. 绘制ax1和ax2的图形,设置label。
4. 收集所有句柄和标签(从ax1和ax2中)。
5. 使用tight_layout调整子图布局,但通过rect参数指定子图区域(不包括图例区域)所占的图形矩形区域。例如,我们希望子图区域只占用图形的一部分,中间留空给图例。
- rect参数是一个四元组(left, bottom, right, top),表示子图区域在图形中的位置(归一化坐标)。通常,我们可以将整个图形分为三个部分:标题(顶部一部分),子图1,图例(一小部分),子图2。所以,我们可以设置:
rect=[0, 0, 1, 0.9] # 表示子图区域占用图形高度的90%,顶部留10%给标题,底部留10%给图例?但这样图例会在底部,而我们需要在中间。
- 因此,我们需要将图形分成四部分(从上到下):
标题:占5%
子图1:占45%
图例区域:占5%
子图2:占45%
这样,图例区域在中间,占5%的高度。
- 但是,tight_layout的rect参数只能指定整个子图区域(包括所有子图)的矩形,不能分别指定每个部分。所以,我们需要整体调整,然后手动设置子图的位置。
由于tight_layout无法如此细致地控制,我们可以改为使用GridSpec。
因此,我们采用GridSpec方案:
步骤:
1. 创建GridSpec,设置行数和高度比例:
- 行数:3行(标题行、子图1行、图例行、子图2行?)实际上应该是4行:标题(顶部)、子图1、图例、子图2。
- 高度比例:我们设置四行的高度比例,例如[0.1, 0.4, 0.1, 0.4](标题10%,子图1占40%,图例10%,子图2占40%)。
2. 在GridSpec的相应位置创建子图。
代码结构:
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
fig = plt.figure(figsize=(6,8))
gs = GridSpec(nrows=4, ncols=1, height_ratios=[0.1, 0.4, 0.1, 0.4])
# 标题区域:我们通常不使用子图来放置标题,可以直接用fig.text或fig.suptitle,但为了布局,我们也可以用一个子图来占位(然后隐藏边框)。
# 但标题可以直接放在图形顶部,所以我们用第一个区域放置标题(作为文本),第二个区域为ax1,第三个区域放置图例(不是子图,而是图例),第四个区域为ax2。
# 创建ax1和ax2
ax1 = fig.add_subplot(gs[1,0]) # 第二行第一列
ax2 = fig.add_subplot(gs[3,0]) # 第四行第一列
# 然后,在ax1和ax2中绘制图形。
# 绘制示例:在ax1中画一条线
line1, = ax1.plot([0,1,2], [0,1,0], label='Line 1')
# 在ax2中画一条线
line2, = ax2.plot([0,1,2], [0,0,1], label='Line 2')
# 设置整个图形的标题,放在第一行位置
fig.suptitle('总标题', fontsize=16)
# 收集图例的句柄和标签
handles = [line1, line2]
labels = [h.get_label() for h in handles]
# 在第三行(即图例区域)创建图例,注意第三行是一个GridSpec的位置,我们可以在这个位置创建一个没有边框的子图,然后在这个子图上创建图例,或者直接用fig.legend并将位置定位到第三行的中间。
# 方法:使用fig.legend,并指定位置为第三行的位置(归一化坐标)
# 我们可以获取第三行的边界框(bbox)位置,然后在这个位置放置图例。
# 另一种更简单的方法:在第三行的位置添加一个子图(隐藏坐标轴),然后在这个子图上调用legend,但这样需要调整子图的边距等。
# 或者,直接计算第三行在图形中的坐标:
# 由于我们有GridSpec,我们可以获取第三行的bbox(在图形坐标中)
# 但更简单的是,我们使用fig.legend,然后用bbox_to_anchor参数指定中间位置,并设置bbox_transform为fig.transFigure(即使用图形坐标)。
# 图形坐标:第三行的中心位置,x=0.5(横向居中),y=第三行区域的中心(注意GridSpec从下到上,四行分别索引0,1,2,3,第三行索引为2,高度比例为0.1,位置在从下到上的0.5(子图2的高度0.4)和0.1(图例)和0.4(子图1)之上)?
# 实际上,高度比例是[0.1, 0.4, 0.1, 0.4],所以从下往上:
底部0-0.4:子图2(索引3)
0.4-0.5:图例区域的下半部分(索引2)?不对,GridSpec的排列是从上往下还是从下往上?
# GridSpec默认是从上往下排列,第一行在最上面,第二行在下面,等等。所以索引0(第一行)在最上面,高度比例为0.1,然后是索引1(第二行)高度0.4,然后是索引2(第三行)高度0.1,最后索引3(第四行)高度0.4在底部。
# 所以,四个区域从顶部到底部的位置:
区域0(标题):从y=0.9到1.0(因为第一个区域占10%)
区域1(ax1):从y=0.5到0.9(占40%)
区域2(图例):从y=0.4到0.5(占10%)
区域3(ax2):从y=0.0到0.4(占40%)
# 因此,图例区域(区域2)的中心坐标为(0.5, 0.45)(因为图例区域从0.4到0.5,中心为0.45)。
# 所以,我们可以这样创建图例:
fig.legend(handles, labels, loc='center', bbox_to_anchor=(0.5, 0.45), ncol=2) # 一行排列,所以ncol等于标签个数(这里2个)
# 最后,我们隐藏第三行子图(如果我们没有创建子图,就不需要隐藏)。注意,我们没有在第三行创建子图,所以不需要隐藏。
# 显示图形
plt.show()
但是,这里有一个问题:GridSpec只用于创建ax1和ax2,标题和图例我们是用fig.suptitle和fig.legend添加的,这样布局就是按照GridSpec划分的区域。
注意:我们并没有在区域0(标题区域)创建子图,所以标题是直接放在图形的顶部,而图例放在区域2(即图例区域)的中心。
这样,我们就实现了:
- 标题在子图1上方(实际上在整个图形的顶部,而子图1在标题下方)
- 图例在两个子图中间(区域2)
这种方法可以精确控制位置。
但如果我们不想使用GridSpec(因为它需要更多代码),我们可以尝试更简便的方法:
1. 创建两个子图(垂直排列),并设置整个图形大小。
2. 使用fig.suptitle设置标题。
3. 绘制图形。
4. 使用fig.tight_layout()调整子图,然后手动调整子图的位置(通过调整subplots_adjust)来为图例留出中间空间。
例如:
plt.subplots_adjust(top=0.9, bottom=0.1, hspace=0.5) # 增加两个子图之间的间距(hspace)
5. 然后,将图例放在两个子图之间的空白区域:通过bbox_to_anchor=(0.5, 0.5)(注意这个0.5是图形坐标,而两个子图中间的位置可能是0.5,但实际上因为调整了间距,所以具体位置需要计算)。
但是,由于hspace是子图之间的间距(以子图高度的比例为单位),而整个子图区域在图形中的位置由subplots_adjust控制,所以图例的垂直位置可以通过以下方式计算:
整个子图区域(包括两个子图及其之间的空白)的底部和顶部由subplots_adjust的bottom和top指定。
两个子图之间的空白区域位于整个子图区域的中间部分。
计算起来比较复杂,不如GridSpec直观。
因此,我们选择GridSpec方法。
下面给出完整代码示例:
```python
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
# 创建图形和GridSpec布局
fig = plt.figure(figsize=(6,8))
# 我们将图形分为4行:标题(0.1)、子图1(0.4)、图例(0.1)、子图2(0.4)
height_ratios = [0.1, 0.4, 0.1, 0.4]
gs = GridSpec(nrows=4, ncols=1, height_ratios=height_ratios, figure=fig)
# 创建子图ax1(在第二行,索引1)和ax2(在第四行,索引3)
ax1 = fig.add_subplot(gs[1,0])
ax2 = fig.add_subplot(gs[3,0])
# 在ax1中绘制示例数据
line1, = ax1.plot([0, 1, 2], [0, 1, 0.5], 'b-', label='Line 1')
scatter1 = ax1.scatter([0, 1, 2], [0.5, 0, 1], color='red', label='Scatter 1')
# 在ax2中绘制示例数据
line2, = ax2.plot([0, 1, 2], [1, 0, 0.5], 'g--', label='Line 2')
scatter2 = ax2.scatter([0, 1, 2], [0.5, 1, 0], color='purple', label='Scatter 2')
# 设置整个图形的标题(放在第一行区域,即gs[0]的位置,但我们不在该区域创建子图,所以用fig.suptitle)
# 注意:fig.suptitle默认在图形顶部,我们可以调整其垂直位置使其位于第一行区域内
# 第一行区域在图形坐标中从top=1.0向下到1.0-height_ratios[0]=0.9,所以中心位置在0.95
fig.suptitle('Two Rows Subplot Example', fontsize=16, y=0.95) # y=0.95是为了放在第一行区域内
# 收集所有图例的句柄和标签(从两个子图)
handles = [line1, scatter1, line2, scatter2]
labels = [h.get_label() for h in handles]
# 在图例区域(第三行,索引2)的中心位置创建图例
# 图例区域在图形坐标中的y范围:从底部开始,底部是0.0(图形底部),总高度为1.0。
# 根据高度比例,图例区域在y轴的位置:
# 第四行(子图2)高度占0.4,所以从0.0到0.4
# 第三行(图例)高度0.1,所以在0.4到0.5
# 第二行(子图1)高度0.4,所以在0.5到0.9
# 第一行(标题)高度0.1,所以在0.9到1.0
# 因此,图例区域的中心y坐标为:0.4 + 0.1/2 = 0.45
fig.legend(handles, labels, loc='center', bbox_to_anchor=(0.5, 0.45), ncol=4) # 一行排列,所以ncol=4(有4个图例项?这里我们每个子图有两个,共4个)
# 注意:如果图例项太多,一行放不下,可以调整ncol的值,或者调整图例的字体大小。
# 如果需要,我们可以关闭图例区域中的子图(但我们没有创建,所以不需要)
# 显示图形
plt.show()
```
注意:图例区域的高度比例(0.1)可能不足以显示图例,如果图例项太多,可能需要增加该比例(例如0.15)或调整图例的字体大小。
另外,图例的ncol参数应该设置为图例项的数量,这样它们就会排成一行。如果图例项很多,一行排不下,可以分成多行(调整ncol)或减小字体大小。
以上代码实现了需求。
如果用户希望使用更简单的方法(不使用GridSpec),也可以尝试使用plt.subplots配合subplots_adjust调整间距,然后用fig.legend指定位置,但位置需要根据调整后的间距计算,不够精确。
因此,我们推荐使用GridSpec方法。
</think>
根据您的需求,在Python中实现两行一列子图布局,并将标题置于子图1上方、图例放在两子图中间一行排列,可以通过Matplotlib的`GridSpec`结合自定义布局实现。以下是完整解决方案:
```python
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
# 创建图形和GridSpec布局(3行1列)
fig = plt.figure(figsize=(8, 10))
gs = GridSpec(nrows=3, ncols=1, height_ratios=[1, 0.1, 1]) # 中间行预留图例空间
# 子图1(顶部)
ax1 = fig.add_subplot(gs[0])
ax1.plot([0, 1, 2], [0, 1, 0], 'b-', label='正弦波')
ax1.scatter([0, 1, 2], [0.2, 0.8, 0.3], c='red', label='数据点')
ax1.set_title('子图1数据展示', fontsize=14)
# 子图2(底部)
ax2 = fig.add_subplot(gs[2])
ax2.bar(['A', 'B', 'C'], [3, 7, 2], color='green', label='类别统计')
ax2.plot([0, 1, 2], [2, 5, 3], 'm--', label='趋势线')
# 设置整体图形标题(位于子图1上方)
fig.suptitle('双子图数据对比分析', fontsize=16, y=0.95)
# 收集所有图例句柄和标签
handles1, labels1 = ax1.get_legend_handles_labels()
handles2, labels2 = ax2.get_legend_handles_labels()
all_handles = handles1 + handles2
all_labels = labels1 + labels2
# 在中间区域(gs[1])创建图例(一行排列)
legend_ax = fig.add_subplot(gs[1])
legend_ax.axis('off') # 隐藏坐标轴
legend_ax.legend(all_handles, all_labels, loc='center',
ncol=len(all_labels), # 自动适应项数
frameon=False,
fontsize=12)
# 优化布局
plt.tight_layout(pad=3.0) # 增加全局边距
plt.subplots_adjust(top=0.9) # 为顶部标题预留空间
plt.show()
```
### 关键实现原理:
1. **网格布局**:使用`GridSpec(3,1)`创建三行布局:
- 第0行:子图1区域
- 第1行:预留图例放置区(高度设为0.1)
- 第2行:子图2区域
2. **标题定位**:
- 通过`fig.suptitle()`设置全局标题
- `y=0.95`参数确保标题位于子图1上方
- `plt.subplots_adjust(top=0.9)`为标题留出空间[^1]
3. **图例处理**:
- 合并两个子图的图例句柄(`get_legend_handles_labels`)
- 在中间区域创建隐藏坐标轴的子图专门放置图例
- `ncol=len(all_labels)`确保所有图例项水平排成一行[^2]
4. **布局优化**:
- `tight_layout(pad=3.0)`防止元素重叠
- 高度比例参数`height_ratios`精确控制区域占比[^4]
### 效果说明:
1. 标题位于子图1正上方(非图形顶部)
2. 图例水平居中显示在两个子图之间
3. 自动适应图例项数量(一行排列)
4. 保留子图间距防止元素重叠[^1]
> 此方法避免了`constrained_layout`无法调整外部元素的限制[^3],通过显式空间分配实现精确布局控制。