約束式佈局指南#

使用約束式佈局來將繪圖乾淨地放入圖表中。

約束式佈局會自動調整子圖,使刻度標籤、圖例和顏色條等裝飾不會重疊,同時仍保留使用者要求的邏輯佈局。

約束式佈局類似於緊湊佈局,但靈活度更高。它處理放置在多個軸上的顏色條(放置顏色條)、巢狀佈局(subfigures)以及跨越列或行的軸(subplot_mosaic),力求對齊同一行或列中軸的軸脊。此外,壓縮式佈局將嘗試將固定長寬比的軸更靠近。本文檔中將介紹這些功能,以及最後討論的一些實作細節

約束式佈局通常需要在將任何軸新增到圖表之前啟動。有兩種方法可以做到這一點

這些將在以下各節中詳細說明。

警告

呼叫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)
Title

為了防止這種情況,需要調整軸的位置。對於子圖,可以使用Figure.subplots_adjust手動調整子圖參數來完成。但是,使用 layout="constrained" 關鍵字引數指定圖表將自動進行調整。

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

當有多個子圖時,經常會看到不同軸的標籤彼此重疊。

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

在呼叫 plt.subplots 時指定 layout="constrained" 會導致佈局被正確約束。

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

顏色條#

如果使用 Figure.colorbar 建立顏色條,則需要為其騰出空間。約束式佈局會自動執行此操作。請注意,如果指定 use_gridspec=True,它將被忽略,因為此選項是為了透過 tight_layout 改善佈局。

注意

對於 pcolormesh 關鍵字引數(pc_kwargs),我們使用字典來保持本文檔中的呼叫一致。

arr = np.arange(100).reshape((10, 10))
norm = mcolors.Normalize(vmin=0., vmax=100.)
# see note above: this makes all pcolormesh calls consistent:
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)
constrainedlayout guide

如果向 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)
constrainedlayout guide

如果從軸網格內部指定軸列表,顏色條將適當地佔用空間並留下間隙,但所有子圖的大小仍會相同。

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)
constrainedlayout guide

Suptitle#

約束式佈局也可以為 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')
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))
constrainedlayout guide

但是,這將從子圖佈局中佔用空間

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))
constrainedlayout guide

為了使圖例或其他繪圖物件佔用子圖佈局的空間,我們可以 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)
# trigger a draw so that constrained layout is executed once
# before we turn it off when printing....
fig.canvas.draw()
# we want the legend included in the bbox_inches='tight' calcs.
leg.set_in_layout(True)
# we don't want the layout to change at this point.
fig.set_layout_engine('none')
try:
    fig.savefig('../../../doc/_static/constrained_layout_1b.png',
                bbox_inches='tight', dpi=100)
except FileNotFoundError:
    # this allows the script to keep going if run interactively and
    # the directory above doesn't exist
    pass
constrainedlayout guide

儲存的檔案如下所示

../../../_images/constrained_layout_1b.png

解決這種尷尬情況的更好方法是直接使用 Figure.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:
    # this allows the script to keep going if run interactively and
    # the directory above doesn't exist
    pass
constrainedlayout guide

儲存的檔案如下所示

../../../_images/constrained_layout_2b.png

邊框間距和間隔#

軸之間的邊框間距在水平方向由 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)
constrainedlayout guide

子圖之間的間隔進一步由 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)
constrainedlayout guide

如果有兩個以上的列,則 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)
constrainedlayout guide

GridSpec 也有可選的 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)
# this has no effect because the space set in the gridspec trumps the
# space set in *constrained layout*.
fig.get_layout_engine().set(w_pad=4 / 72, h_pad=4 / 72, hspace=0.0,
                            wspace=0.0)
constrainedlayout guide

使用顏色條的間隔#

顏色條與其父物件之間會保持 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)
pad: 0, pad: 0.05, pad: 0.1, pad: 0.2

rcParams#

有五個 rcParams 可以設定,可以在腳本中或在 matplotlibrc 檔案中設定。它們都有 figure.constrained_layout 的前綴。

  • use:是否使用約束式佈局。預設值為 False。

  • w_padh_pad:軸物件周圍的間距。浮點數表示英吋。預設值為 3./72 英吋 (3 點)。

  • wspacehspace:子圖組之間的間距。浮點數表示分隔的子圖寬度的一部分。預設值為 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)
Title, Title, Title, Title

與 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)
Title, Title

可以實現更複雜的 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)
Title, Title

請注意,在上述範例中,左右兩欄的垂直範圍不同。如果我們希望兩個網格的頂部和底部對齊,則它們需要位於同一個 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')
Overlapping Gridspecs, Title, Title

此範例使用兩個 gridspecs,使顏色條僅適用於一組 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)
Nested plots using subgridspec, Title, Title, title, title, title, title

Matplotlib 現在提供 subfigures 而不是使用 subgridspecs,它也適用於約束式佈局

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')
Nested plots using subfigures, Title, Title, title, title, title, title

手動設定軸位置#

可能會有充分的理由手動設定軸位置。手動呼叫 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])
Title

固定長寬比的軸網格:「壓縮」佈局#

約束式佈局 在軸的「原始」位置網格上運作。但是,當軸具有固定的長寬比時,通常會縮短一側,並在縮短的方向上留下很大的間隙。在以下範例中,軸是正方形,但圖形相當寬,因此存在水平間隙。

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'")
fixed-aspect plots, layout='constrained'

一種明顯的解決方法是使圖形大小更接近正方形,但是,要完全消除間隙,需要經過反覆試驗。對於簡單的軸網格,我們可以使用 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'")
fixed-aspect plots, layout='compressed'

手動關閉約束式佈局#

約束式佈局 通常會在每次繪製圖形時調整軸的位置。如果您想要取得約束式佈局提供的間距,但不希望它更新,請執行初始繪製,然後呼叫 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)
# third Axes that spans both rows in second column:
ax3 = plt.subplot(2, 2, (2, 4))

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

但以下內容會導致佈局不佳。

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')
Mixed nrows, ncols, Title, Title, Title

同樣,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')
subplot2grid, Title, Title, Title, Title

其他注意事項#

  • 約束式佈局 只會考慮刻度標籤、軸標籤、標題和圖例。因此,其他繪圖物件可能會被剪裁,也可能會重疊。

  • 它假設刻度標籤、軸標籤和標題所需的額外空間與軸的原始位置無關。這通常是真的,但在極少數情況下並非如此。

  • 後端處理呈現字型的方式略有差異,因此結果不會完全相同。

  • 當繪圖物件使用超出軸邊界的軸座標時,將繪圖物件新增至軸時,會導致不尋常的佈局。可以透過使用 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)
Title

簡單情況:兩個軸#

當有多個軸時,它們的佈局會以簡單的方式約束。在此範例中,左軸的裝飾比右軸大得多,但它們共用一個下邊距,該邊距會加大以容納較大的 xlabel。與共用的上邊距相同。左右邊距不會共用,因此允許不同。

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

兩個軸和顏色條#

顏色條只是擴展父 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)
constrainedlayout guide

與 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)
constrainedlayout guide

大小不均的軸#

有兩種方法可以在 Gridspec 佈局中使軸大小不均,一種是將它們指定為跨越 Gridspecs 行或列,另一種是指定寬度和高度比率。

此處使用第一種方法。請注意,中間的 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)
constrainedlayout guide

一種需要微調的情況是,如果邊界沒有任何藝術家約束其寬度。在以下情況中,第 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()
Title

腳本總執行時間:(0 分鐘 23.357 秒)

由 Sphinx-Gallery 生成的圖庫