一、布局基础概念
1. 布局三要素
import matplotlib.pyplot as plt
# 核心布局对象
fig = plt.figure() # 画布 (Figure)
ax = fig.add_subplot(111) # 坐标系 (Axes)
ax.axis([0, 1, 0, 1]) # 坐标轴 (Axis)
# 布局关系图示
'''
Figure (画布)
└── Axes1 (坐标系1)
├── AxisX (X轴)
├── AxisY (Y轴)
└── 绘图区域
└── Axes2 (坐标系2)...
'''2. 常用单位
# 图形尺寸单位 fig = plt.figure(figsize=(8, 6)) # 英寸(默认) fig = plt.figure(figsize=(20, 15), dpi=72) # 像素 = 英寸 * dpi # 布局中的常用单位 ''' inches : 英寸(默认) points : 点(1点=1/72英寸) pixels : 像素 figure : 相对于画布的比例 [0,1] axes : 相对于坐标轴的比例 [0,1] data : 数据坐标(取决于数据范围) '''
二、基础布局方法
1. 单图布局
# 方法1:显式创建 fig = plt.figure(figsize=(10, 8)) ax = fig.add_subplot(111) # 1行1列,第1个 ax.plot([1, 2, 3], [1, 4, 9]) # 方法2:隐式创建(MATLAB风格) plt.figure(figsize=(10, 8)) plt.subplot(111) # 创建当前坐标轴 plt.plot([1, 2, 3], [1, 4, 9]) # 方法3:推荐方式(面向对象) fig, ax = plt.subplots(figsize=(10, 8)) ax.plot([1, 2, 3], [1, 4, 9])
2. 多子图布局 – subplots
# 创建网格布局
fig, axes = plt.subplots(nrows=2, ncols=3, # 2行3列
figsize=(12, 8),
sharex=True, # 共享X轴
sharey=True) # 共享Y轴
# axes是2x3的数组
for i in range(2):
for j in range(3):
ax = axes[i, j]
ax.plot(np.random.randn(50))
ax.set_title(f'({i},{j})')
ax.grid(True)
plt.suptitle('2×3子图网格', fontsize=16)
plt.tight_layout() # 自动调整间距
plt.show()3. 访问子图的不同方式
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
# 方式1:索引访问(推荐)
axes[0, 0].plot([1, 2, 3], [1, 2, 3])
axes[0, 1].scatter(np.random.rand(10), np.random.rand(10))
axes[1, 0].bar(['A', 'B', 'C'], [3, 7, 2])
axes[1, 1].hist(np.random.randn(1000), bins=30)
# 方式2:展开为一维数组
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
axes_flat = axes.flatten() # 转为1D数组
for i, ax in enumerate(axes_flat):
ax.plot(np.random.randn(50))
ax.set_title(f'Plot {i+1}')
# 方式3:使用subplot位置索引
fig = plt.figure(figsize=(10, 8))
ax1 = plt.subplot(221) # 2行2列第1个
ax2 = plt.subplot(222) # 2行2列第2个
ax3 = plt.subplot(212) # 2行1列第2个(跨两列)三、高级布局系统
1. GridSpec – 灵活网格布局
import matplotlib.gridspec as gridspec
fig = plt.figure(figsize=(12, 10))
gs = gridspec.GridSpec(3, 3, # 3行3列网格
figure=fig,
width_ratios=[1, 2, 1], # 列宽比例
height_ratios=[2, 1, 1], # 行高比例
wspace=0.3, # 列间距
hspace=0.4) # 行间距
# 创建不同大小的子图
ax1 = fig.add_subplot(gs[0, :]) # 第1行,所有列(跨3列)
ax2 = fig.add_subplot(gs[1, :2]) # 第2行,前2列
ax3 = fig.add_subplot(gs[1:, 2]) # 第2行到最后,第3列(跨2行)
ax4 = fig.add_subplot(gs[2, 0]) # 第3行,第1列
ax5 = fig.add_subplot(gs[2, 1]) # 第3行,第2列
# 填充内容
ax1.plot(np.random.randn(100))
ax1.set_title('主图(跨3列)')
ax2.scatter(np.random.rand(50), np.random.rand(50))
ax2.set_title('散点图')
ax3.bar(['A', 'B', 'C', 'D'], np.random.rand(4))
ax3.set_title('柱状图')
ax4.hist(np.random.randn(1000), bins=30)
ax4.set_title('直方图')
ax5.pie([30, 25, 20, 25], labels=['A', 'B', 'C', 'D'])
ax5.set_title('饼图')
plt.tight_layout()
plt.show()2. GridSpecFromSubplotSpec – 嵌套网格
fig = plt.figure(figsize=(12, 8))
# 外层网格:1行2列
outer_gs = gridspec.GridSpec(1, 2, figure=fig, width_ratios=[1, 2])
# 左图:单个子图
ax_left = fig.add_subplot(outer_gs[0])
ax_left.plot(np.random.randn(100))
ax_left.set_title('时间序列')
# 右图:内层网格(2行2列)
inner_gs = gridspec.GridSpecFromSubplotSpec(2, 2,
subplot_spec=outer_gs[1],
wspace=0.3, hspace=0.4)
# 在内层网格中添加子图
titles = ['散点图', '柱状图', '直方图', '箱线图']
plot_funcs = ['scatter', 'bar', 'hist', 'boxplot']
for i in range(4):
ax = fig.add_subplot(inner_gs[i])
if i == 0:
ax.scatter(np.random.rand(50), np.random.rand(50))
elif i == 1:
ax.bar(['A', 'B', 'C', 'D'], np.random.rand(4))
elif i == 2:
ax.hist(np.random.randn(1000), bins=30)
else:
ax.boxplot([np.random.randn(100) for _ in range(5)])
ax.set_title(titles[i])
plt.suptitle('嵌套网格布局示例', fontsize=16)
plt.tight_layout()
plt.show()四、相对定位与绝对定位
1. add_axes – 绝对坐标定位
fig = plt.figure(figsize=(10, 8))
# 主图(相对画布定位)
ax_main = fig.add_axes([0.1, 0.1, 0.8, 0.8]) # [left, bottom, width, height]
ax_main.plot(np.random.randn(100))
ax_main.set_title('主图')
# 插入小图(插图)
ax_inset = fig.add_axes([0.6, 0.6, 0.3, 0.3]) # 在主图内插入
ax_inset.plot(np.random.randn(20), 'r-')
ax_inset.set_title('局部放大')
# 边角小图
ax_corner = fig.add_axes([0.05, 0.05, 0.2, 0.2]) # 左下角
ax_corner.hist(np.random.randn(1000), bins=30, alpha=0.7)
ax_corner.set_title('分布')
# 多小图示例
fig2 = plt.figure(figsize=(12, 8))
# 九宫格布局
for i in range(3):
for j in range(3):
left = 0.1 + j * 0.25
bottom = 0.1 + i * 0.25
ax = fig2.add_axes([left, bottom, 0.2, 0.2])
ax.plot(np.random.randn(20))
ax.set_title(f'({i},{j})')
ax.set_xticks([])
ax.set_yticks([])
plt.suptitle('绝对定位布局', fontsize=16)
plt.show()2. Divider – 比例尺布局
from mpl_toolkits.axes_grid1 import make_axes_locatable
fig, ax = plt.subplots(figsize=(10, 8))
# 创建主图
im = ax.imshow(np.random.rand(10, 10), cmap='viridis')
# 使用Divider在主图右侧添加颜色条
divider = make_axes_locatable(ax)
cax = divider.append_axes("right", size="5%", pad=0.1) # 右侧,5%宽度,0.1英寸间距
plt.colorbar(im, cax=cax)
# 添加其他轴
ax_hist = divider.append_axes("top", size="20%", pad=0.1) # 顶部,20%高度
ax_hist.hist(im.get_array().flatten(), bins=30, orientation='horizontal')
ax_hist.set_xlabel('频数')
ax_hist.invert_xaxis()
ax.set_title('使用Divider的布局')
plt.show()五、布局调整与间距控制
1. tight_layout – 自动调整
fig, axes = plt.subplots(2, 3, figsize=(12, 8))
for i, ax in enumerate(axes.flat):
ax.plot(np.random.randn(50))
ax.set_title(f'长长的标题 {i+1}' * 3) # 故意创建长标题
ax.set_xlabel('X轴标签')
ax.set_ylabel('Y轴标签')
# 自动调整(会尝试调整子图参数以避免重叠)
plt.tight_layout(pad=1.0, # 整体边距
w_pad=0.5, # 子图间水平间距
h_pad=1.0, # 子图间垂直间距
rect=[0, 0, 1, 0.95]) # 调整区域 [left, bottom, right, top]
plt.suptitle('使用tight_layout自动调整', y=0.98)
plt.show()2. constrained_layout – 约束布局(更智能)
# 创建时启用
fig, axes = plt.subplots(2, 2, figsize=(10, 8),
constrained_layout=True) # 启用约束布局
for i, ax in enumerate(axes.flat):
ax.plot(np.random.randn(50))
ax.set_title(f'子图 {i+1} 带有长标题' * 2)
ax.set_xlabel('X轴')
ax.set_ylabel('Y轴')
# 或者之后启用
fig.set_constrained_layout(True)
# 调整参数
fig.set_constrained_layout_pads(w_pad=0.04, h_pad=0.04,
hspace=0.02, wspace=0.02)
plt.suptitle('使用constrained_layout', fontsize=16)
plt.show()3. 手动调整子图位置
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
# 获取子图位置
for i, ax in enumerate(axes.flat):
pos = ax.get_position() # 获取当前位置 [left, bottom, width, height]
print(f'子图{i}位置: {pos}')
# 手动调整位置
new_pos = [pos.x0 + 0.02, pos.y0 + 0.02,
pos.width * 0.9, pos.height * 0.9]
ax.set_position(new_pos)
ax.plot(np.random.randn(50))
ax.set_title(f'子图 {i+1}')
plt.suptitle('手动调整子图位置', fontsize=16)
plt.show()六、特殊布局模式
1. 不对称布局
fig = plt.figure(figsize=(12, 8))
# 定义不对称布局
gs = gridspec.GridSpec(3, 3, figure=fig)
ax1 = fig.add_subplot(gs[0, :]) # 顶部通栏
ax2 = fig.add_subplot(gs[1, :2]) # 中部左侧
ax3 = fig.add_subplot(gs[1:, 2]) # 右侧栏
ax4 = fig.add_subplot(gs[2, 0]) # 左下角
ax5 = fig.add_subplot(gs[2, 1]) # 中下角
# 填充内容
axes = [ax1, ax2, ax3, ax4, ax5]
plot_types = ['line', 'scatter', 'bar', 'hist', 'pie']
colors = ['blue', 'green', 'red', 'orange', 'purple']
for ax, plot_type, color in zip(axes, plot_types, colors):
if plot_type == 'line':
ax.plot(np.random.randn(100), color=color)
elif plot_type == 'scatter':
ax.scatter(np.random.rand(50), np.random.rand(50), color=color)
elif plot_type == 'bar':
ax.bar(['A', 'B', 'C'], np.random.rand(3), color=color)
elif plot_type == 'hist':
ax.hist(np.random.randn(1000), bins=30, color=color, alpha=0.7)
elif plot_type == 'pie':
ax.pie([30, 20, 25, 25], colors=[color]*4)
ax.set_title(f'{plot_type.capitalize()} Chart')
plt.suptitle('不对称布局示例', fontsize=16, y=0.95)
plt.tight_layout()
plt.show()2. 多图共享坐标轴
fig, axes = plt.subplots(3, 1, figsize=(10, 12), sharex=True)
# 生成相关数据
x = np.linspace(0, 10, 100)
data1 = np.sin(x)
data2 = np.cos(x)
data3 = np.sin(x) * np.cos(x)
# 绘制共享X轴的图表
axes[0].plot(x, data1, 'b-', linewidth=2)
axes[0].set_ylabel('sin(x)')
axes[0].grid(True)
axes[0].set_title('共享X轴的多图表', fontsize=14, pad=20)
axes[1].plot(x, data2, 'r--', linewidth=2)
axes[1].set_ylabel('cos(x)')
axes[1].grid(True)
axes[2].plot(x, data3, 'g-.', linewidth=2)
axes[2].set_ylabel('sin(x)cos(x)')
axes[2].set_xlabel('X轴')
axes[2].grid(True)
# 调整垂直间距
plt.subplots_adjust(hspace=0.1) # 减小垂直间距(因为共享X轴)
plt.show()3. 图文混排布局
from matplotlib.patches import Rectangle
fig = plt.figure(figsize=(14, 10))
# 使用GridSpec创建复杂布局
gs = gridspec.GridSpec(4, 4, figure=fig,
width_ratios=[3, 1, 3, 1],
height_ratios=[1, 3, 1, 3])
# 主图
ax_main = fig.add_subplot(gs[1:3, 0:2])
x = np.linspace(0, 2*np.pi, 100)
ax_main.plot(x, np.sin(x), 'b-', label='sin(x)')
ax_main.plot(x, np.cos(x), 'r--', label='cos(x)')
ax_main.legend()
ax_main.set_title('三角函数图', fontsize=14)
ax_main.grid(True)
# 右侧说明区域
ax_text = fig.add_subplot(gs[1:3, 2])
ax_text.axis('off') # 隐藏坐标轴
text_content = """
三角函数说明:
正弦函数 (sin):
• 周期: 2π
• 范围: [-1, 1]
• 奇函数: sin(-x) = -sin(x)
余弦函数 (cos):
• 周期: 2π
• 范围: [-1, 1]
• 偶函数: cos(-x) = cos(x)
关系式:
sin²(x) + cos²(x) = 1
"""
ax_text.text(0, 0.5, text_content,
transform=ax_text.transAxes,
verticalalignment='center',
fontsize=11,
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))
# 底部统计图
ax_stats = fig.add_subplot(gs[3, 0:2])
categories = ['sin均值', 'sin方差', 'cos均值', 'cos方差']
values = [np.sin(x).mean(), np.sin(x).var(),
np.cos(x).mean(), np.cos(x).var()]
bars = ax_stats.bar(categories, values, color=['blue', 'lightblue', 'red', 'pink'])
ax_stats.set_title('统计指标')
ax_stats.set_ylabel('数值')
# 顶部标题
fig.suptitle('三角函数分析报告', fontsize=18, y=0.95)
plt.tight_layout()
plt.show()七、实战:仪表板布局
# 创建仪表板布局
fig = plt.figure(figsize=(16, 12))
# 使用GridSpec定义仪表板布局
gs = gridspec.GridSpec(12, 12, figure=fig) # 12x12的精细网格
# 1. 顶部标题
ax_title = fig.add_subplot(gs[0, :])
ax_title.axis('off')
ax_title.text(0.5, 0.5, '数据分析仪表板',
horizontalalignment='center',
verticalalignment='center',
fontsize=24, fontweight='bold')
# 2. 主K线图(假设) - 左上大图
ax_main = fig.add_subplot(gs[1:6, 0:8])
dates = pd.date_range('2023-01-01', periods=100, freq='D')
prices = 100 + np.cumsum(np.random.randn(100))
ax_main.plot(dates, prices, 'b-', linewidth=2)
ax_main.fill_between(dates, prices.min(), prices, alpha=0.3)
ax_main.set_title('价格走势', fontsize=14)
ax_main.grid(True, alpha=0.3)
# 3. 右侧指标面板
ax_metrics = fig.add_subplot(gs[1:4, 8:])
ax_metrics.axis('off')
metrics_text = """
关键指标:
================
当前价格: $102.34
日变化: +2.34 (+2.34%)
周变化: +5.67 (+5.89%)
月变化: +12.34 (+13.71%)
技术指标:
================
RSI: 54.32
MACD: 0.23
布林带宽度: 4.56
"""
ax_metrics.text(0.05, 0.95, metrics_text,
transform=ax_metrics.transAxes,
verticalalignment='top',
fontsize=11,
bbox=dict(boxstyle='round', facecolor='lightgray', alpha=0.3))
# 4. 底部子图1:成交量
ax_volume = fig.add_subplot(gs[6:9, 0:4])
volume = np.random.randint(1000, 10000, 100)
ax_volume.bar(range(100), volume, alpha=0.7, color='gray')
ax_volume.set_title('成交量', fontsize=12)
ax_volume.grid(True, alpha=0.3)
# 5. 底部子图2:收益率分布
ax_returns = fig.add_subplot(gs[6:9, 4:8])
returns = np.diff(prices) / prices[:-1]
ax_returns.hist(returns, bins=30, alpha=0.7, color='green', edgecolor='black')
ax_returns.axvline(x=returns.mean(), color='red', linestyle='--', label=f'均值: {returns.mean():.2%}')
ax_returns.set_title('收益率分布', fontsize=12)
ax_returns.legend(fontsize=9)
ax_returns.grid(True, alpha=0.3)
# 6. 底部子图3:相关性热图
ax_corr = fig.add_subplot(gs[9:, 0:4])
# 模拟相关性矩阵
corr_matrix = np.corrcoef(np.random.randn(5, 100))
im = ax_corr.imshow(corr_matrix, cmap='coolwarm', vmin=-1, vmax=1)
ax_corr.set_title('相关性矩阵', fontsize=12)
ax_corr.set_xticks(range(5))
ax_corr.set_yticks(range(5))
ax_corr.set_xticklabels(['AAPL', 'GOOG', 'MSFT', 'AMZN', 'TSLA'])
ax_corr.set_yticklabels(['AAPL', 'GOOG', 'MSFT', 'AMZN', 'TSLA'])
plt.colorbar(im, ax=ax_corr, fraction=0.046, pad=0.04)
# 7. 底部子图4:饼图
ax_pie = fig.add_subplot(gs[9:, 4:8])
sectors = ['科技', '金融', '医疗', '能源', '消费']
weights = [35, 25, 20, 10, 10]
ax_pie.pie(weights, labels=sectors, autopct='%1.1f%%', startangle=90)
ax_pie.set_title('行业权重', fontsize=12)
# 8. 右侧底部:控制面板
ax_control = fig.add_subplot(gs[9:, 8:])
ax_control.axis('off')
control_text = """
控制面板:
================
时间范围:
▢ 1天 ▢ 1周
▢ 1月 ☑ 3月
▢ 6月 ▢ 1年
指标选择:
☑ MA(5) ▢ MA(10)
☑ MA(20) ▢ MA(60)
☑ 成交量 ▢ RSI
刷新频率:
▢ 实时 ☑ 5分钟
▢ 15分钟 ▢ 1小时
"""
ax_control.text(0.05, 0.95, control_text,
transform=ax_control.transAxes,
verticalalignment='top',
fontsize=10,
bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.2))
# 调整整体布局
plt.subplots_adjust(left=0.05, right=0.95, top=0.95, bottom=0.05,
wspace=0.3, hspace=0.4)
plt.show()八、布局最佳实践
1. 选择布局策略
# 根据需求选择布局方法
layout_strategies = {
'简单网格': 'plt.subplots()',
'灵活网格': 'gridspec.GridSpec()',
'绝对定位': 'fig.add_axes()',
'嵌套布局': 'GridSpecFromSubplotSpec',
'自动调整': 'tight_layout() 或 constrained_layout',
}
# 决策流程
'''
1. 是否需要不规则布局? → 是 → GridSpec
2. 是否是简单网格? → 是 → plt.subplots()
3. 是否需要精确定位? → 是 → add_axes()
4. 是否有重叠问题? → 是 → tight_layout
5. 是否复杂仪表板? → 是 → 混合使用
'''2. 常见问题解决
# 问题1:标题重叠
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
# 解决方案:
plt.tight_layout(pad=2.0) # 增加边距
# 问题2:颜色条与子图重叠
fig, ax = plt.subplots(figsize=(8, 6))
im = ax.imshow(np.random.rand(10, 10))
# 解决方案:
from mpl_toolkits.axes_grid1 import make_axes_locatable
divider = make_axes_locatable(ax)
cax = divider.append_axes("right", size="5%", pad=0.05)
plt.colorbar(im, cax=cax)
# 问题3:子图大小不一致
fig = plt.figure(figsize=(12, 8))
# 解决方案:使用width_ratios和height_ratios
gs = gridspec.GridSpec(2, 2,
width_ratios=[2, 1],
height_ratios=[1, 2])3. 性能优化
# 1. 预分配图形大小 fig = plt.figure(figsize=(12, 8)) # 预先确定合适大小 # 2. 避免频繁重绘 plt.ioff() # 关闭交互模式(批量绘图时) # ... 绘图代码 ... plt.ion() # 重新打开 # 3. 使用constrained_layout替代tight_layout(性能更好) fig, axes = plt.subplots(2, 2, constrained_layout=True) # 4. 简化复杂图形 # 对于非常复杂的仪表板,考虑使用多个图形或简化设计
总结建议
- 从简单开始:先用
plt.subplots(),不够用时再考虑GridSpec - 善用自动调整:优先使用
constrained_layout=True - 保持一致性:相同类型的图表使用相同的尺寸和间距
- 考虑可读性:确保标签、标题不重叠,有足够的边距
- 测试不同尺寸:在保存和显示时测试不同DPI和尺寸的效果
Matplotlib的布局系统虽然复杂,但提供了极大的灵活性。掌握这些布局技巧可以创建出专业级的数据可视化仪表板。
