概述
在使用 matplotlib 库进行数据可视化的过程中,我们都离不开一个重要载体画布(Figure)。一个 Figure 对象是所有绘图元素的顶层容器,想要实现画布的合理使用,分区使用必不可少。这里引入了一个新的概念:子区。子区顾名思义就是将大画布划分成若干个子画布,这些子画布共同构成绘图区域。因此,子区的本质是在纵横交错的行列网格中添加坐标轴系统。
在 matplotlib 中跟子区相关的函数一共有三个,分别是 plt.subplots()
、plt.subplot()
、subplot2grid()
。接下来本文将会详细介绍每一个函数。
subplots 函数
概述
该命令可以非常便捷地创建一个行列布局的子区列表,同时创建一个画布对象(Figure),其返回值是 (fig, ax)
。方法签名如下:
def subplots(nrows=1,
ncols=1,
*,
sharex=False,
sharey=False,
squeeze=True,
subplot_kw=None,
gridspec_kw=None,
**fig_kw):
参数说明:
- nrows:整数型,指定子图网格的行数;
- ncols:整数型,指定子图网格的列数;
- sharex:布尔型或字符型,指定是否共享 x 轴;
- sharey:布尔型或字符型,指定是否共享 y 轴;
- squeeze:布尔型,指定是否进行压缩
- subplot_kw:字典型,指定创建每个子图的调用的关键字参数;
- gridspec_kw:字典型,指定子图放置的网格的关键字参数;
- fig_kw:字典型,指定传递给 pyplot. figure 方法的关键字参数;
- 返回值:(Figure 对象、Axes 对象)
- fig_kw 的参数传递给
pyplot.figure
方法的意思是该参数的所有内容会直接传递给plt.figure
方法。subplot_kw、subplot_kw 同理;- squeeze 参数,如果为 True 且仅构造一个子图,则返回的单个 Axes 对象将作为标量返回。对于 Nx1或1xM 子图,返回的对象是包含 Axes 对象的 numpy 数组。对于 N×M 个子区,该方法会返回一个二维阵列。如果为 False,则完全不进行压缩:返回的 Axes 对象始终是包含 Axes 实例的2D 数组,即使它只有一个子区。
源码分析如下:
if self.get_constrained_layout():
gs = GridSpec(nrows, ncols, figure=self, gridspec_kw) #创建GridSpec类实例
else:
# this should turn constrained_layout off if we don't want it
gs = GridSpec(nrows, ncols, figure=None, gridspec_kw)
self._gridspecs.append(gs)
# Create array to hold all axes.
axarr = np.empty((nrows, ncols), dtype=object) #创建一个空矩阵,类型是Axes对象
#循环创建坐标轴系统,下标0开始
for row in range(nrows):
for col in range(ncols):
shared_with = {"none": None, "all": axarr[0, 0],"row": axarr[row, 0], "col": axarr[0, col]} #sharex和sharey的检查字典
subplot_kw["sharex"] = shared_with[sharex] #字典取值
subplot_kw["sharey"] = shared_with[sharey] #字典取值
axarr[row, col] = self.add_subplot(gs[row, col], subplot_kw) #创建坐标轴系统
# turn off redundant tick labeling
#设置共享X轴
if sharex in ["col", "all"]:
# turn off all but the bottom row
for ax in axarr[:-1, :].flat:
ax.xaxis.set_tick_params(which='both', labelbottom=False, labeltop=False)
ax.xaxis.offsetText.set_visible(False)
#设置共享Y轴
if sharey in ["row", "all"]:
# turn off all but the first column
for ax in axarr[:, 1:].flat:
ax.yaxis.set_tick_params(which='both', labelleft=False, labelright=False)
ax.yaxis.offsetText.set_visible(False)
if squeeze:
return axarr.item() if axarr.size == 1 else axarr.squeeze() #调用squeeze方法压删除一维
else:
return axarr #返回一个二维矩阵
总结一下,首先创建 GridSpec 对象,该类用于指定网格的几何形状;然后循环创建坐标轴系统,并添加到一个空矩阵中;接下来两个 if-for
代码块用来设置 X 轴和 Y 轴的共享;最后对 squeeze 参数进行检测,看是否对 axarr对象进行压缩。
示例
首先创建一个1行1列的网格布局,squeeze参数为False,看看其返回值:
fig, ax = plt.subplots(squeeze=False)
ax ->
接着设置设置多行和多列,看看返回值:
fig, ax = plt.subplots(2, 2, squeeze=True)
ax -> array([[, ],
[, ]], dtype=object))
fig, ax = plt.subplots(2, 2, squeeze=False)
ax -> array([[, ],
[, ]], dtype=object))
最后是一个综合示例,完整代码如下:
fig, ax = plt.subplots(2, 3)
colors_list = ['#8dd3c7', '#ffffb3', '#bebada']
ax[0, 0].bar([1, 2, 3], [0.6, 0.2, 0.8], color=colors_list, width=0.8, hatch='///', align='center')
ax[0, 0].errorbar([1, 2, 3], [0.6, 0.2, 0.8], yerr=0.1, capsize=0, ecolor='#377eb8', fmt='o:')
ax[0, 1].errorbar([1, 2, 3], [20, 30, 36], xerr=2, ecolor='#4daf4a', elinewidth=2, fmt='s', label='ETN')
ax[0, 1].legend(loc=3, fancybox=True, shadow=True, fontsize=10, borderaxespad=0.4)
ax[0, 1].set_ylim(10, 40)
ax[0, 1].set_xlim(-2, 6)
x3 = np.arange(1, 10, 0.5)
y3 = np.cos(x3)
ax[0, 2].stem(x3, y3, basefmt='r-', linefmt='b-.', markerfmt='bo', label='life signal')
ax[0, 2].legend(loc=2, fancybox=True, shadow=True, frameon=False, fontsize=8, borderaxespad=0.6, borderpad=0.0)
ax[0, 2].set_ylim(-2, 2)
ax[0, 2].set_xlim(0, 11)
x4 = np.linspace(0, 2*np.pi, 500)
x4_1 = np.linspace(0, 2*np.pi, 1000)
y4 = np.cos(x4)*np.exp(-x4)
y4_1 = np.sin(2*x4_1)
line1, line2 = ax[1, 0].plot(x4, y4, 'k--', x4_1, y4_1, 'r-', lw=2)
ax[1, 0].legend((line1, line2), ('energy', 'patience'), loc=2, fancybox=True, shadow=True, framealpha=0.3,mode='expand', fontsize=8, ncol=2, columnspacing=2, borderpad=0.1, markerscale=0.1)
ax[1, 0].set_ylim(-2, 2)
ax[1, 0].set_xlim(0, 2*np.pi)
x5 = np.random.rand(100)
ax[1, 1].boxplot(x5, vert=False, showmeans=True, meanprops=dict(color='black'))
ax[1, 1].set_yticks([])
ax[1, 1].set_xlim(-1.1, 1.1)
ax[1, 1].set_ylabel('Miscro SD Card')
ax[1, 1].text(-1.0, 1.2, 'net weight', fontsize=15, style='italic', weight='black', family='monospace')
mu = 0.0
sigma = 1.0
x6 = np.random.randn(10000)
n, bins, patches = ax[1, 2].hist(x6, bins=30, histtype='stepfilled', cumulative=True, color='cornflowerblue',
label='Test', density=True)
y = ((1 / (np.sqrt(2 * np.pi) * sigma)) * np.exp(-0.5 * (1 / sigma * (bins-mu)) 2))
y = y.cumsum()
y /= y[-1]
ax[1, 2].plot(bins, y, 'r--', lw=1.5, label='Theory')
ax[1, 2].set_ylim([0.0, 1.1])
ax[1, 2].grid(ls=':', lw=1, color='grey', alpha=0.5)
ax[1, 2].legend(fontsize=8, shadow=True, fancybox=True, framealpha=0.8)
ax[1, 2].autoscale()
plt.show()
画图结果如下:
subplot 方法
概述
该方法用于绘制网格区域中的几何形状相同的子区布局。本质上是对 Figure.add_subplot()
方法的包装,深入一点讲,它可以检索 Figure 对象所有的 Axes 对象,也可以创建一个新的 Axes 对象并将其添加到当前的 Figure 对象上。以下是它的源码分析:
fig = gcf() #调用plt.gcf获得当前Figure对象
# First, search for an existing subplot with a matching spec.
key = SubplotSpec._from_subplot_args(fig, args)
#检索撇配现有坐标轴系统
for ax in fig.axes:
# if we found an axes at the position sort out if we can re-use it
if hasattr(ax, 'get_subplotspec') and ax.get_subplotspec() == key:
# if the user passed no kwargs, re-use
if kwargs == {}:
break
# if the axes class and kwargs are identical, reuse
elif ax._projection_init == fig._process_projection_requirements(
*args, **kwargs):
break
else:
# we have exhausted the known Axes and none match, make a new one!
ax = fig.add_subplot(*args, **kwargs) #如果没有匹配则依据参数创建相应的子图
fig.sca(ax) #将当前轴设置为ax,将当前Figure设置为ax的parent
小结一下,这个方法先调用 gcf()
方法获取当前 Figure 对象;接着对该对象的坐标轴系统进行检索匹配,如果能找到对应索引的 Axes 对象则返回该对象,否则调用 add_subplots()
方法创建创建一个新的 Axes 对象;最后调用 sca()
方法将检索或者新创建的 Axes 对象设置为当前子图,即调用 gca()
的返回值。
subplot
有多种形式,其部分示例如下:
subplot(nrows, ncols, index, kwargs)
subplot(pos, kwargs)
subplot(kwargs)
subplot(ax)
参数说明:
- nrows:整数型,指定子图网格的行数;
- ncols:整数型,指定子图网格的列数;
- index:整数型,指定当前激活的子图索引,索引从左上角的1开始并向右增加。索引也可以是两元素元组指定
(start, end)
,即绘制一个跨区子图; - projection:指定坐标轴投影类型;
- polar:指定是否使用极坐标系,等价于
projection='polar'
; - sharex:指定是否共享 X 轴;
- sharey:指定是否共享 Y 轴;
- label:指定返回的 Axes 对象的名称;
- 返回值:返回 Axes 对象;
- polar 这个参数尽量不要用,所有的投影参数最好都使用 projection 参数来传递;
- 使用此函数的时候索引下标都是从1开始。而不是0,切记!!!
示例
首先介绍四种地球投影方式,分别是 Aitoff、Hammer、Lambert、Mollweide。
Aitoff 映射程序如下:
plt.figure()
plt.subplot(projection="aitoff")
plt.title("Aitoff")
plt.grid(True)
画图结果如下:
Hammer 映射程序如下:
plt.figure()
plt.subplot(projection="hammer")
plt.title("Hammer")
plt.grid(True)
画图结果如下:
Lambert 映射程序如下:
plt.figure()
plt.subplot(projection="lambert")
plt.title("Lambert")
plt.grid(True)
画图结果如下:
Mollweide 映射程序如下:
plt.figure()
plt.subplot(projection="mollweide")
plt.title("Mollweide")
plt.grid(True)
画图结果如下:
最后介绍一种最常用的映射即 polar 映射,也就是极坐标系,polar 映射映射程序如下:
radi = np.linspace(0, 1, 100)
theta = 2*np.pi*radi
ax = plt.subplot(111, polar=True)
ax.plot(theta, radi, color='r', lw=2)
plt.show()
画图结果如下:
综合示例
import matplotlib.pyplot as plt
import numpy as np
plt.rcParams["font.sans-serif"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False
plt.style.use("classic")
x = np.linspace(1, 100, 200)
y = np.random.normal(75.0, 15.0, 200)
plt.figure().set_facecolor("#FFF")
ax = plt.subplot(2, 2, (1, 2))
ax.plot(y + np.random.randn(200), ls='-', lw=2, color='c', label=r'$\sin(x)$')
ax.plot(y + np.random.randn(200), ls='-', lw=2, color='r', label=r'$\cos(x)$')
ax.legend(title='noise', title_fontsize=15, loc='upper right')
ax = plt.subplot(2, 2, 3)
ax.plot(y, ls='-', lw=2, color='c', label=r'$\sin(x)$')
ax.legend(loc='upper right')
ax = plt.subplot(2, 2, 4)
ax.plot(y, ls='-', lw=2, color='r', label=r'$\cos(x)$')
ax.legend(loc='upper right')
plt.show()
画图结果如下:
subplot2grid 方法
前面介绍的子区函数只能绘制等分画布的图形样式,要想按照绘图区域的不同展示目的,进行非等分画布形式的图形展示,需要向画布多次调用 plt.subplot()
方法完成非等分画布的展示任务,但是频繁的调用显得过于麻烦,而且在划分画布的过程中容易出现疏漏和差错。因此,需要使用一种更高级的定制网格方法,这个方法就是 subplot2grid ()
,通过指定 rowspan
和 colspan
参数可以让子区跨越固定的网格布局的多个行和列,实现自定义子区分区。该方法的签名如下:
def matplotlib.pyplot.subplot2grid(shape, loc, rowspan=1, colspan=1, fig=None, **kwargs):
...
参数说明:
- shape:指定整个网格的行列数;
- loc:Axes 对象所处的位置;
- rowspan:指定该 Axes 对象的行跨度;
- colspan:指定该 Axes 对象的列跨度;
- fig:指定要进行切分的 Figure 对象;
- kwargs:指定向
add_subplot()
方法传递的关键字参数; - 返回值:Axes 对象;
- 图形位置的索引起点是 0,而不像
subplot()
方法那样从 1 开始。- 网格的实现基于 gridspec 模块,该模块是一个可以指定画布中子区位置或者说是布局的模块,该模块有一个类 GridSpec,它可以指定网格的几何形状,在各个方法的底层可以看到都是调用
fig.add_subplot(gs)
。
示例
接下来使用一个示例来演示该方法的使用,完整代码如下:
import matplotlib.pyplot as plt
import numpy as np
plt.style.use("classic")
plt.rcParams["font.sans-serif"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False
plt.figure().set_facecolor("#FFF")
plt.subplot2grid((2, 3), (0, 0), colspan=2)
x = np.linspace(0, 4, 100)
y = np.random.randn(100)
plt.scatter(x, y, c='c')
plt.subplot2grid((2, 3), (0, 2))
plt.title("空白区域")
plt.subplot2grid((2, 3), (1, 0), colspan=3)
x = np.linspace(0, 4, 100)
y1 = np.sin(x)
plt.plot(x, y1, lw=2, ls='-')
plt.xlim(0, 3)
plt.grid(True, ls=':', c='r')
plt.title("折线图")
plt.suptitle("subplot2grid函数实例演示", fontsize=25)
plt.show()
画图结果如下:
往期回顾
- 【matplotlib】可视化解决方案——如何在绘图区域嵌套子绘图区域
- 【matplotlib】可视化解决方案——如何正确设置图例
- 【matplotlib】可视化解决方案——如何正确设置轴长度和范围
- 【matplotlib】可视化解决方案——如何正确理解pyplot和OO-API
- 【matplotlib】可视化解决方案——如何正确展示和保存图像
- 【matplotlib】可视化解决方案——如何设置字符串的输出字体效果
- 【matplotlib】可视化解决方案——如何正确使用颜色映射表
- 【matplotlib】可视化解决方案——如何调整轴脊位置
- 【matplotlib】可视化解决方案——如何设置坐标系计量方法
- 【matplotlib】可视化解决方案——如何正确使用文本注释
文中难免会出现一些描述不当之处(尽管我已反复检查多次),欢迎在留言区指正,相关的知识点也可进行分享,希望大家都能有所收获!!如果觉得我的文章写得还行,不妨支持一下。你的每一个转发、关注、点赞、评论都是对我最大的支持!