Matplotlib 全面使用指南 -- 受约束布局指南 Constrained layout guide

文中内容仅限技术学习与代码实践参考,市场存在不确定性,技术分析需谨慎验证,不构成任何投资建议。

Matplotlib

受约束布局指南

使用 受约束布局 来干净地将绘图放置在图形中。

受约束布局 会自动调整子图,使得刻度标签、图例和色条等装饰物不会重叠,同时仍保留用户请求的逻辑布局。

受约束布局 与紧凑布局类似,但更加灵活。它可以处理放置在多个轴上的色条(放置色条)、嵌套布局(子图)以及跨行或跨列的轴(subplot_mosaic),力求对齐同一行或同一列中的轴的脊线。此外,压缩布局将尝试将固定纵横比的轴更紧密地移动在一起。本文档描述了这些功能,并在最后讨论了一些实现细节

受约束布局 通常需要在将任何轴添加到图形之前激活。有两种方法可以做到这一点:

  • 使用 subplotsfiguresubplot_mosaic 等的相应参数,例如:

    plt.subplots(layout="constrained")
    
  • 通过 rcParams 激活,如下所示:

    plt.rcParams['figure.constrained_layout.use'] = True
    

这些将在以下各节中详细描述。

警告:调用 tight_layout 将关闭 受约束布局

简单示例

使用默认的轴定位,轴标题、轴标签或刻度标签有时会超出图形区域,因此被裁剪。

import matplotlib.pyplot as plt
import numpy as np

import matplotlib.colors as mcolors
import matplotlib.gridspec as gridspec

plt.rcParams['savefig.facecolor'] = "0.8"
plt.rcParams['figure.figsize'] = 4.5, 4.
plt.rcParams['figure.max_open_warning'] = 50


def example_plot(ax, fontsize=12, hide_labels=False):
    ax.plot([1, 2])

    ax.locator_params(nbins=3)
    if hide_labels:
        ax.set_xticklabels([])
        ax.set_yticklabels([])
    else:
        ax.set_xlabel('x-label', fontsize=fontsize)
        ax.set_ylabel('y-label', fontsize=fontsize)
        ax.set_title('Title', fontsize=fontsize)

fig, ax = plt.subplots(layout=None)
example_plot(ax, fontsize=24)

img

为了防止这种情况,需要调整轴的位置。对于子图,可以通过使用 Figure.subplots_adjust 手动调整子图参数来完成。然而,使用 layout="constrained" 关键字参数指定图形将自动进行调整。

fig, ax = plt.subplots(layout="constrained")
example_plot(ax, fontsize=24)

img

当您有多个子图时,通常您会看到不同轴的标签相互重叠。

fig, axs = plt.subplots(2, 2, layout=None)
for ax in axs.flat:
    example_plot(ax)

img

在调用 plt.subplots 时指定 layout="constrained" 会导致布局被正确约束。

fig, axs = plt.subplots(2, 2, layout="constrained")
for ax in axs.flat:
    example_plot(ax)

img

色条

如果您使用 Figure.colorbar 创建色条,则需要为其腾出空间。受约束布局 会自动执行此操作。请注意,如果您指定 use_gridspec=True,它将被忽略,因为此选项是为了通过 tight_layout 改进布局而设置的。

注意:对于 pcolormesh 关键字参数(pc_kwargs),我们使用字典来保持整个文档中的调用一致。

arr = np.arange(100).reshape((10, 10))
norm = mcolors.Normalize(vmin=0., vmax=100.)
# 参见上文注释:这使得所有 pcolormesh 调用保持一致:
pc_kwargs = {'rasterized': True, 'cmap': 'viridis', 'norm': norm}
fig, ax = plt.subplots(figsize=(4, 4), layout="constrained")
im = ax.pcolormesh(arr, **pc_kwargs)
fig.colorbar(im, ax=ax, shrink=0.6)

img

如果您将轴列表(或其他可迭代容器)指定给 colorbarax 参数,受约束布局 将从指定的轴中腾出空间。

fig, axs = plt.subplots(2, 2, figsize=(4, 4), layout="constrained")
for ax in axs.flat:
    im = ax.pcolormesh(arr, **pc_kwargs)
fig.colorbar(im, ax=axs, shrink=0.6)

img

如果您从轴网格内部指定轴列表,色条将适当地占用空间,并留下一个间隙,但所有子图仍将保持相同的大小。

fig, axs = plt.subplots(3, 3, figsize=(4, 4), layout="constrained")
for ax in axs.flat:
    im = ax.pcolormesh(arr, **pc_kwargs)
fig.colorbar(im, ax=axs[1:, 1], shrink=0.8)
fig.colorbar(im, ax=axs[:, -1], shrink=0.6)

img

总标题

受约束布局 还可以为 suptitle 腾出空间。

fig, axs = plt.subplots(2, 2, figsize=(4, 4), layout="constrained")
for ax in axs.flat:
    im = ax.pcolormesh(arr, **pc_kwargs)
fig.colorbar(im, ax=axs, shrink=0.6)
fig.suptitle('Big Suptitle')

图例

图例可以放置在其父轴之外。约束布局旨在通过 Axes.legend() 处理此问题。但是,约束布局目前无法处理通过 Figure.legend() 创建的图例。

fig, ax = plt.subplots(layout="constrained")
ax.plot(np.arange(10), label='This is a plot')
ax.legend(loc='center left', bbox_to_anchor=(0.8, 0.5))

img

但是,这会占用子图布局的空间:

fig, axs = plt.subplots(1, 2, figsize=(4, 2), layout="constrained")
axs[0].plot(np.arange(10))
axs[1].plot(np.arange(10), label='This is a plot')
axs[1].legend(loc='center left', bbox_to_anchor=(0.8, 0.5))

img

为了防止图例或其他绘图工具占用子图布局的空间,我们可以调用 leg.set_in_layout(False)。当然,这可能会导致图例最终被裁剪,但如果随后使用 fig.savefig(‘outname.png’, bbox_inches=‘tight’) 调用绘图,则此功能非常有用。但请注意,必须再次切换图例的 get_in_layout 状态才能使保存的文件正常工作,并且如果我们希望约束布局在打印前调整轴的大小,则必须手动触发绘制。

fig, axs = plt.subplots(1, 2, figsize=(4, 2), layout="constrained")

axs[0].plot(np.arange(10))
axs[1].plot(np.arange(10), label='This is a plot')
leg = axs[1].legend(loc='center left', bbox_to_anchor=(0.8, 0.5))
leg.set_in_layout(False)
# 触发绘制,以便执行一次约束布局
# 在打印时关闭它之前……
fig.canvas.draw()
# 我们希望将图例包含在 bbox_inches='tight' 计算中。
leg.set_in_layout(True)
# 我们不希望此时布局发生变化。
fig.set_layout_engine('none')
try:
    fig.savefig('../../../doc/_static/constrained_layout_1b.png',
                bbox_inches='tight', dpi=100)
except FileNotFoundError:
    # 这样,如果以交互方式运行脚本,并且
    # 上述目录不存在,脚本仍可继续运行
    pass

img

保存的文件如下所示:

img

解决此尴尬问题的更好方法是直接使用 Figure.legend 提供的 legend 方法:

fig, axs = plt.subplots(1, 2, figsize=(4, 2), layout="constrained")
axs[0].plot(np.arange(10))
lines = axs[1].plot(np.arange(10), label='This is a plot')
labels = [l.get_label() for l in lines]
leg = fig.legend(lines, labels, loc='center left',
                 bbox_to_anchor=(0.8, 0.5), bbox_transform=axs[1].transAxes)
try:
    fig.savefig('../../../doc/_static/constrained_layout_2b.png',
                bbox_inches='tight', dpi=100)
except FileNotFoundError:
    # 这样,如果以交互方式运行脚本,并且
    # 上述目录不存在,脚本仍可继续运行
    pass

img

保存的文件如下所示:

img

填充和间距

轴之间的填充在水平方向上由 w_padwspace 控制,在垂直方向上由 h_padhspace 控制。这些可以通过 set 进行编辑。w/h_pad 是轴周围的最小空间,单位为英寸:

fig, axs = plt.subplots(2, 2, layout="constrained")
for ax in axs.flat:
    example_plot(ax, hide_labels=True)
fig.get_layout_engine().set(w_pad=4 / 72, h_pad=4 / 72, hspace=0,
                            wspace=0)

img

子图之间的间距由 wspacehspace 进一步设置。这些被指定为子图组整体大小的一部分。如果这些值小于 w_padh_pad,则使用固定的填充。请注意,在下面的示例中,边缘的空间与上面的示例相比没有变化,但子图之间的空间发生了变化。

fig, axs = plt.subplots(2, 2, layout="constrained")
for ax in axs.flat:
    example_plot(ax, hide_labels=True)
fig.get_layout_engine().set(w_pad=4 / 72, h_pad=4 / 72, hspace=0.2,
                            wspace=0.2)

img

如果有两列以上,则 wspace 在它们之间共享,因此在这里,wspace 被分成两部分,每列之间的 wspace 为 0.1:

fig, axs = plt.subplots(2, 3, layout="constrained")
for ax in axs.flat:
    example_plot(ax, hide_labels=True)
fig.get_layout_engine().set(w_pad=4 / 72, h_pad=4 / 72, hspace=0.2,
                            wspace=0.2)

img

GridSpecs 还具有可选的 hspacewspace 关键字参数,将使用这些参数代替由 受约束布局 设置的填充:

fig, axs = plt.subplots(2, 2, layout="constrained",
                        gridspec_kw={'wspace': 0.3, 'hspace': 0.2})
for ax in axs.flat:
    example_plot(ax, hide_labels=True)
# 这没有效果,因为在 gridspec 中设置的空间会覆盖
# 在 *受约束布局* 中设置的空间。
fig.get_layout_engine().set(w_pad=4 / 72, h_pad=4 / 72, hspace=0.0,
                            wspace=0.0)

img

色条间距

色条放置在距其父对象 pad 的距离处,其中 pad 是父对象宽度的分数。到下一个子图的间距由 w/hspace 给出。

fig, axs = plt.subplots(2, 2, layout="constrained")
pads = [0, 0.05, 0.1, 0.2]
for pad, ax in zip(pads, axs.flat):
    pc = ax.pcolormesh(arr, **pc_kwargs)
    fig.colorbar(pc, ax=ax, shrink=0.6, pad=pad)
    ax.set_xticklabels([])
    ax.set_yticklabels([])
    ax.set_title(f'pad: {pad}')
fig.get_layout_engine().set(w_pad=2 / 72, h_pad=2 / 72, hspace=0.2,
                            wspace=0.2)

img

rcParams

有五个 rcParams 可以设置,无论是在脚本中还是在 matplotlibrc 文件中。它们都以 figure.constrained_layout 为前缀:

  • use: 是否使用 受约束布局。默认值为 False

  • w_pad, h_pad: 轴对象周围的填充。
    表示英寸的浮点数。默认值为 3./72. 英寸(3 磅)

  • wspace, hspace: 子图组之间的间距。
    表示被分隔的子图宽度一部分的浮点数。
    默认值为 0.02。

plt.rcParams['figure.constrained_layout.use'] = True
fig, axs = plt.subplots(2, 2, figsize=(3, 3))
for ax in axs.flat:
    example_plot(ax)

img

与 GridSpec 一起使用

受约束布局 旨在与 subplots()subplot_mosaic()GridSpec()add_subplot() 一起使用。

请注意,在下面的示例中,layout="constrained"

plt.rcParams['figure.constrained_layout.use'] = False
fig = plt.figure(layout="constrained")

gs1 = gridspec.GridSpec(2, 1, figure=fig)
ax1 = fig.add_subplot(gs1[0])
ax2 = fig.add_subplot(gs1[1])

example_plot(ax1)
example_plot(ax2)

img

更复杂的 gridspec 布局是可能的。请注意,这里我们使用便利函数 add_gridspecsubgridspec

fig = plt.figure(layout="constrained")

gs0 = fig.add_gridspec(1, 2)

gs1 = gs0[0].subgridspec(2, 1)
ax1 = fig.add_subplot(gs1[0])
ax2 = fig.add_subplot(gs1[1])

example_plot(ax1)
example_plot(ax2)

gs2 = gs0[1].subgridspec(3, 1)

for ss in gs2:
    ax = fig.add_subplot(ss)
    example_plot(ax)
    ax.set_title("")
    ax.set_xlabel("")

ax.set_xlabel("x-label", fontsize=12)

img

请注意,在上面的示例中,左列和右列的垂直范围不相同。如果我们希望两个网格的顶部和底部对齐,那么它们需要在同一个 gridspec 中。我们还需要使这个图形更大,以便轴不会塌陷到零高度:

fig = plt.figure(figsize=(4, 6), layout="constrained")

gs0 = fig.add_gridspec(6, 2)

ax1 = fig.add_subplot(gs0[:3, 0])
ax2 = fig.add_subplot(gs0[3:, 0])

example_plot(ax1)
example_plot(ax2)

ax = fig.add_subplot(gs0[0:2, 1])
example_plot(ax, hide_labels=True)
ax = fig.add_subplot(gs0[2:4, 1])
example_plot(ax, hide_labels=True)
ax = fig.add_subplot(gs0[4:, 1])
example_plot(ax, hide_labels=True)
fig.suptitle('Overlapping Gridspecs')

img

此示例使用两个 gridspec 使色条仅与一组 pcolors 相关。请注意,由于此原因,左列比右侧的两列宽。当然,如果您希望子图大小相同,则只需要一个 gridspec。请注意,使用 subfigures 也可以达到相同的效果。

fig = plt.figure(layout="constrained")
gs0 = fig.add_gridspec(1, 2, figure=fig, width_ratios=[1, 2])
gs_left = gs0[0].subgridspec(2, 1)
gs_right = gs0[1].subgridspec(2, 2)

for gs in gs_left:
    ax = fig.add_subplot(gs)
    example_plot(ax)
axs = []
for gs in gs_right:
    ax = fig.add_subplot(gs)
    pcm = ax.pcolormesh(arr, **pc_kwargs)
    ax.set_xlabel('x-label')
    ax.set_ylabel('y-label')
    ax.set_title('title')
    axs += [ax]
fig.suptitle('Nested plots using subgridspec')
fig.colorbar(pcm, ax=axs)

img

与其使用子 gridspecs,Matplotlib 现在提供了 subfigures,它们也适用于 受约束布局

fig = plt.figure(layout="constrained")
sfigs = fig.subfigures(1, 2, width_ratios=[1, 2])

axs_left = sfigs[0].subplots(2, 1)
for ax in axs_left.flat:
    example_plot(ax)

axs_right = sfigs[1].subplots(2, 2)
for ax in axs_right.flat:
    pcm = ax.pcolormesh(arr, **pc_kwargs)
    ax.set_xlabel('x-label')
    ax.set_ylabel('y-label')
    ax.set_title('title')
fig.colorbar(pcm, ax=axs_right)
fig.suptitle('Nested plots using subfigures')

img

手动设置轴位置

可能有很好的理由手动设置轴位置。手动调用 set_position 将设置轴,使得 受约束布局 不再对其产生影响。(请注意,受约束布局 仍为移动的轴留出空间)。

fig, axs = plt.subplots(1, 2, layout="constrained")
example_plot(axs[0], fontsize=12)
axs[1].set_position([0.2, 0.2, 0.4, 0.4])

img

固定纵横比轴的网格:“压缩”布局

受约束布局 在轴的“原始”位置网格上进行操作。然而,当轴具有固定纵横比时,一侧通常会更短,并在缩短方向上留下较大的间隙。在下面的示例中,轴是正方形的,但图形非常宽,因此存在水平间隙:

fig, axs = plt.subplots(2, 2, figsize=(5, 3),
                        sharex=True, sharey=True, layout="constrained")
for ax in axs.flat:
    ax.imshow(arr)
fig.suptitle("fixed-aspect plots, layout='constrained'")

img

一种明显的解决方法是使图形尺寸更正方形,然而,精确地闭合间隙需要反复试验。对于简单的轴网格,我们可以使用 layout="compressed" 来为我们完成这项工作:

fig, axs = plt.subplots(2, 2, figsize=(5, 3),
                        sharex=True, sharey=True, layout='compressed')
for ax in axs.flat:
    ax.imshow(arr)
fig.suptitle("fixed-aspect plots, layout='compressed'")

img

手动关闭 受约束布局

受约束布局 通常会在每次绘制图形时调整轴的位置。如果您希望获得 受约束布局 提供的间距,但不希望其更新,则进行初始绘制,然后调用 fig.set_layout_engine('none')
这对于动画可能很有用,因为动画中刻度标签的长度可能会发生变化。

请注意,对于使用工具栏的后端,受约束布局 会在 ZOOMPAN GUI 事件期间关闭。这可以防止在缩放和平移期间轴的位置发生变化。

限制

不兼容的函数

受约束布局 可以与 pyplot.subplot 一起使用,但前提是每次调用的行数和列数相同。
原因是每次调用 pyplot.subplot 时,如果几何形状不相同,都会创建一个新的 GridSpec 实例,并且 受约束布局。因此,以下示例运行良好:

fig = plt.figure(layout="constrained")

ax1 = plt.subplot(2, 2, 1)
ax2 = plt.subplot(2, 2, 3)
# 跨越第二列两行的第三个轴:
ax3 = plt.subplot(2, 2, (2, 4))

example_plot(ax1)
example_plot(ax2)
example_plot(ax3)
plt.suptitle('Homogeneous nrows, ncols')

img

但以下示例会导致布局不佳:

fig = plt.figure(layout="constrained")

ax1 = plt.subplot(2, 2, 1)
ax2 = plt.subplot(2, 2, 3)
ax3 = plt.subplot(1, 2, 2)

example_plot(ax1)
example_plot(ax2)
example_plot(ax3)
plt.suptitle('Mixed nrows, ncols')

img

同样,subplot2grid 也适用相同的限制,即 nrows 和 ncols 不能更改,以便布局看起来良好。

fig = plt.figure(layout="constrained")

ax1 = plt.subplot2grid((3, 3), (0, 0))
ax2 = plt.subplot2grid((3, 3), (0, 1), colspan=2)
ax3 = plt.subplot2grid((3, 3), (1, 0), colspan=2, rowspan=2)
ax4 = plt.subplot2grid((3, 3), (1, 2), rowspan=2)

example_plot(ax1)
example_plot(ax2)
example_plot(ax3)
example_plot(ax4)
fig.suptitle('subplot2grid')

img

其他注意事项

  • 受约束布局 仅考虑刻度标签、轴标签、标题和图例。因此,其他艺术家可能会被裁剪,也可能会重叠。

  • 它假设刻度标签、轴标签和标题所需的额外空间与轴的原始位置无关。这通常是正确的,但在极少数情况下并非如此。

  • 后端在渲染字体方面存在微小差异,因此结果不会像素相同。

  • 使用超出轴边界的轴坐标的艺术家在添加到轴时会导致不寻常的布局。这可以通过使用 add_artist() 将艺术家直接添加到 Figure 来避免。有关示例,请参见 ConnectionPatch

调试

受约束布局 可能会以某种意想不到的方式失败。因为它使用约束求解器,求解器可能会找到数学上正确但用户根本不想要的解决方案。通常的失败模式是所有尺寸都崩溃到其最小允许值。如果发生这种情况,则有两个原因:

  1. 没有足够的空间容纳您请求绘制的元素。

  2. 存在一个错误 - 在这种情况下,请在 matplotlib/matplotlib#issues 上打开一个问题。

如果存在错误,请报告一个自包含的示例,该示例不需要外部数据或依赖项(除了 numpy)。

算法说明

约束算法相对简单,但由于我们可以布局图形的复杂方式,因此存在一些复杂性。

Matplotlib 中的布局通过 GridSpec 类使用 gridspecs 进行。gridspec 是将图形逻辑划分为行和列,这些行和列中轴的相对宽度由 width_ratiosheight_ratios 设置。

受约束布局 中,每个 gridspec 都会有一个与之关联的 layoutgridlayoutgrid 为每一列有一系列 leftright 变量,为每一行有 bottomtop 变量,并且进一步为左、右、底和顶各有一个边距。在每一行中,底/顶边距会加宽,直到容纳该行中的所有装饰物。同样,对于列和左/右边距也是如此。

简单情况:一个轴

对于单个轴,布局很简单。图形有一个父级 layoutgrid,由一列和一行组成,而包含轴的 gridspec 有一个子级 layoutgrid,同样由一行和一列组成。为轴的每一侧的“装饰物”腾出空间。在代码中,这通过 do_constrained_layout() 中的条目实现,如下所示:

gridspec._layoutgrid[0, 0].edit_margin_min('left',
      -bbox.x0 + pos.x0 + w_pad)

其中 bbox 是轴的紧密边界框,pos 是其位置。请注意,四个边距如何包含轴装饰物。

from matplotlib._layoutgrid import plot_children

fig, ax = plt.subplots(layout="constrained")
example_plot(ax, fontsize=24)
plot_children(fig)

img

简单情况:两个轴

当存在多个轴时,它们的布局以简单的方式绑定。在此示例中,左轴的装饰物比右轴大得多,但它们共享一个底边距,该边距足够大以容纳较大的 xlabel。同样,共享的顶边距也是如此。左和右边距不共享,因此允许不同。

fig, ax = plt.subplots(1, 2, layout="constrained")
example_plot(ax[0], fontsize=32)
example_plot(ax[1], fontsize=8)
plot_children(fig)

img

两个轴和色条

色条只是扩展父级 layoutgrid 单元格边距的另一个项目:

fig, ax = plt.subplots(1, 2, layout="constrained")
im = ax[0].pcolormesh(arr, **pc_kwargs)
fig.colorbar(im, ax=ax[0], shrink=0.6)
im = ax[1].pcolormesh(arr, **pc_kwargs)
plot_children(fig)

img

与 Gridspec 关联的色条

如果色条属于网格的多个单元格,则它会为每个单元格创建一个更大的边距:

fig, axs = plt.subplots(2, 2, layout="constrained")
for ax in axs.flat:
    im = ax.pcolormesh(arr, **pc_kwargs)
fig.colorbar(im, ax=axs, shrink=0.6)
plot_children(fig)

img

大小不一的轴

有两种方法可以使轴在 Gridspec 布局中具有不同的大小,要么通过指定它们跨越 Gridspec 行或列,要么通过指定宽度和高度比。

第一种方法在此处使用。请注意,中间的 topbottom 边距不受左列的影响。这是算法的明智决定,并导致右侧两个轴具有相同高度的情况,但它不是左侧轴高度的 1/2。这与 gridspec 在没有 受约束布局 时的工作方式一致。

fig = plt.figure(layout="constrained")
gs = gridspec.GridSpec(2, 2, figure=fig)
ax = fig.add_subplot(gs[:, 0])
im = ax.pcolormesh(arr, **pc_kwargs)
ax = fig.add_subplot(gs[0, 1])
im = ax.pcolormesh(arr, **pc_kwargs)
ax = fig.add_subplot(gs[1, 1])
im = ax.pcolormesh(arr, **pc_kwargs)
plot_children(fig)

img

一种需要微调的情况是边距没有任何艺术家来约束其宽度。在下面的示例中,第 0 列的右边距和第 3 列的左边距没有任何边距艺术家来设置其宽度,因此我们采用具有艺术家的边距宽度的最大值。这使得所有轴都具有相同的大小:

fig = plt.figure(layout="constrained")
gs = fig.add_gridspec(2, 4)
ax00 = fig.add_subplot(gs[0, 0:2])
ax01 = fig.add_subplot(gs[0, 2:])
ax10 = fig.add_subplot(gs[1, 1:3])
example_plot(ax10, fontsize=14)
plot_children(fig)
plt.show()

img

Download Jupyter notebook: constrainedlayout_guide.ipynb

Download Python source code: constrainedlayout_guide.py

Download zipped: constrainedlayout_guide.zip

风险提示与免责声明
本文内容基于公开信息研究整理,不构成任何形式的投资建议。历史表现不应作为未来收益保证,市场存在不可预见的波动风险。投资者需结合自身财务状况及风险承受能力独立决策,并自行承担交易结果。作者及发布方不对任何依据本文操作导致的损失承担法律责任。市场有风险,投资须谨慎。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

船长Q

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值