【问题标题】:Tight layout using Cartopy without passing w_pad argument?使用 Cartopy 的紧凑布局而不传递 w_pad 参数?
【发布时间】:2020-07-14 19:11:10
【问题描述】:

tight_layout 与 Cartopy 和多个子图一起使用时,结果不是紧凑的布局。示例:

import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import cartopy.crs as ccrs
   
gs = gridspec.GridSpec(2,2)

fig = plt.figure()
for k in range(0,4):
    ax = plt.subplot(gs[k], projection = ccrs.PlateCarree())
    ax.set_extent([0,50,0,90])
    ax.coastlines()
gs.tight_layout(fig)

在每张地图之间产生一个差距很大的图形:

使用pyplot的tight_layout时也会出现同样的问题,不管我设置ax.set_extent()与否,结果都不如预期。示例:

import matplotlib.pyplot as plt
import cartopy.crs as ccrs

fig, ((ax1,ax2),(ax3,ax4)) = plt.subplots(2, 2,
                             subplot_kw=dict(projection=ccrs.PlateCarree()))
for ax in fig.axes:
    ax.coastlines()
plt.tight_layout()

虽然我可以通过在tight_layout 上使用w_padh_pad 参数来手动调整这两种情况,但我必须根据子图的数量和/或地图本身对每个数字进行手动调整。有没有办法在没有这些参数的情况下做到这一点,或者自动确定这些值应该是什么?或者自动设置图形大小?

使用 Linux 5.4.0-40-generic、Python 3.7.6 64 位、Cartopy 0.18.0、Matplotlib 3.2.2

编辑:

问题是如何使用 Cartopy 重现 tight_layout 函数,而无需根据我正在绘制的内容手动设置 w_padh_pad

目前,我正在创建许多不同的图形,不断变化:

  • 投影:有些是 PlateCarree,有些是 Polar Stereographic
  • 地图范围:整个地图,PlateCarree 上减少的纬度/经度,Polar Stereographic 上减少的纬度
  • 不同数量的行/列:1 到 7 行,1 到 5 列。
  • 在第一列和最后一行添加纬度和经度刻度,每列添加标题,第一列添加y_label,以及总标题。
  • 根据图,在每行右侧添加一个颜色条。

最终结果应该是每个子图和不同图形之间具有一致的水平和垂直空间的图形,就像tight_layout 的预期结果一样。

上面所有的示例代码,遵循@swatchai 的回答:

import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import cartopy.crs as ccrs
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
from cartopy.examples.waves import sample_data
from mpl_toolkits.axes_grid1.inset_locator import inset_axes

ar = 1.0  # initial aspect ratio for first trial
wi = 5   # width in inches
hi = wi * ar  # height in inches

# set number of rows/columns
rows, cols = 2,2
# set projection
# proj = ccrs.PlateCarree()
proj = ccrs.NorthPolarStereo()
# set lon/lat extent
# extent = [0,50,0,90] # example for PlateCarree
# extent = [-180,180,-90,90] # complete map
extent = [-180,180,30,90] # example for PolarStereo

gs = gridspec.GridSpec(rows, cols)

# Set figsize using wi and hi
fig = plt.figure(figsize=(wi, hi))
for r in range(0,rows):
    for c in range(0,cols):
        ax = plt.subplot(gs[r,c], projection = proj)
        ax.set_extent(extent,ccrs.PlateCarree())
        ax.coastlines()
        
        # Add sample data
        x, y, z = sample_data((20, 40))
        z = z * -1.5 * y
        cf = ax.contourf(x, y, z, transform=ccrs.PlateCarree())
        
        # Add colorbar
        if c == cols -1:
            axins = inset_axes(ax,
                    bbox_to_anchor=(1.05, 0., 1, 1),
                    width='5%',height='98%',loc='lower left',
                    bbox_transform=ax.transAxes,
                    borderpad=0)
             
            cbar = fig.colorbar(cf, cax=axins,orientation='vertical')
           
        # Gridlines, labels, titles
        if r == 0:
            ax.set_title('column title')
        if c == 0:
            ax.set_ylabel('y label')
            ax.set_yticks([])
        if proj == ccrs.PlateCarree():
            gl = ax.gridlines(draw_labels=False, crs=ccrs.PlateCarree())
            gl.xformatter, gl.yformatter  = LONGITUDE_FORMATTER,  LATITUDE_FORMATTER
            if r == rows-1:
                gl.xlabels_bottom = True
            if c == 0:
                gl.ylabels_left = True 
                ax.set_ylabel('y label',labelpad=35)
                ax.set_yticks([])
        elif proj == ccrs.NorthPolarStereo():
            ax.gridlines()
plt.suptitle('A figure title')
        
        
# Do this to get updated positions/dimensions   
plt.draw() 

# # Get proper ratio here
# # Computed from the last `ax` (being the current)
xmin, xmax = ax.get_xbound()
ymin, ymax = ax.get_ybound()
y2x_ratio = (ymax-ymin)/(xmax-xmin) * rows/cols

# # Apply new h/w aspect ratio by changing h
# # Also possible to change w using set_figwidth()
fig.set_figheight(wi * y2x_ratio)

gs.tight_layout(fig)
plt.show()

上面生成的图表具有非常不同的水平/垂直间隙,具体取决于子图/投影的数量。标签、标题等有时也与整体数字不成比例(tight_layout 按预期工作时不会发生这种情况)。有些还旋转。例如,使用上面的代码和rows, cols = 3, 4PlateCarree() 并绘制完整的地图,给出:

【问题讨论】:

  • 我忘了提到我的代码中的wi = 5 是用户必须指定以容纳所有子图的整个图形的总宽度(以英寸为单位)。在你的最后一个情节中,5 显然太小了,wi = 17 应该会产生更好的情节。请记住,您总是需要一些合理的尺寸才能开始。
  • @swatchai 我明白了。你是怎么想出 17 是这个特定情节的正确数字?试错?如果是,那么使用此代码并手动调整宽度与在tight_layout 上手动调整参数w_padh_pad 有什么好处?
  • 在这种特殊情况下,任何相当大的值,比如 15-20 都足以容纳 4 列图(或每列 3+ 英寸)。大一点不疼。如果您不使用inset_axes(),则步骤tight_layout() 在大多数情况下会很好地调整整体尺寸。使用inset_axes() 会破坏tight_layout() 的功能,因此不允许使用它的一些选项,包括w_padh_pad。图形的所有尺寸(w 和 h)必须由用户指定(或跳过并使用默认值)。我只定义了width,而height 是通过程序计算的。

标签: python-3.x matplotlib cartopy


【解决方案1】:

为了获得更好的绘图,您必须以适当的宽度和高度声明figsize,以使高度/宽度比与子绘图大小相匹配。可以在代码中以编程方式计算比率,然后将其作为更新值应用并根据需要获取图表。

import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import cartopy.crs as ccrs

# use figsize=(w,h) of proper values can give good result
# the aspect ratio, h/w is important
# default is around 1, is good in general, but not in your case

ar = 1.0  # initial aspect ratio for first trial
wi = 5    # width in inches
hi = wi * ar  # height in inches

gs = gridspec.GridSpec(2,2)

# Set figsize using wi and hi
fig = plt.figure(figsize=(wi, hi))  

for k in range(0,4):
    ax = plt.subplot(gs[k], projection = ccrs.PlateCarree())  
    # any of these projections are OK to use
    # PlateCarree() Robinson() Mercator() TransverseMercator() Orthographic()
    ax.set_extent([0,50,0,90])
    ax.coastlines()
    ax.gridlines()

# Do this to get updated positions/dimensions   
plt.draw() 

# Get proper ratio here
xmin, xmax = ax.get_xbound()
ymin, ymax = ax.get_ybound()
y2x_ratio = (ymax-ymin)/(xmax-xmin)

# This also gives same ratio
#x1, x2 = ax.get_xlim()
#y1, y2 = ax.get_ylim()
#y2x_ratio = (y2-y1)/(x2-x1)

print("y2x_ratio: "+ str(y2x_ratio)) 
#1.8258 with Robinson; 1.8 PlateCarree; 3.37 Mercator; 1.549 TransverseMercator

# Apply new h/w aspect ratio by changing h
# Also possible to change w using set_figwidth()
fig.set_figheight(wi * y2x_ratio)

gs.tight_layout(fig)
plt.show()

输出图:

编辑 1

最初的答案集中在一个 2x2(行 x 列)的数组上。对于其他数组,必须更改纵横比的计算以解决此问题。这是可运行代码和示例图的更新版本。

import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import cartopy.crs as ccrs

# use figsize=(w,h) of proper values can give good result
# the aspect ratio, h/w is important
# default is around 1, is good in general cases

ar = 1.0  # initial aspect ratio for first trial
wi = 5    # width of the whole figure in inches, ...
# set it wide enough to cover all columns of sub-plots

hi = wi * ar  # height in inches

# set number of rows/columns
rows, cols = 4,3

gs = gridspec.GridSpec(rows, cols)

# Set figsize using wi and hi
fig = plt.figure(figsize=(wi, hi))

for k in range(0, rows*cols):
    ax = plt.subplot(gs[k], projection = ccrs.Robinson())  
    # any of these projections are OK to use
    # PlateCarree() Robinson() Mercator() Orthographic()
    #ax.set_extent([0,50,0,90])
    ax.coastlines()
    ax.gridlines()

# Do this to get updated positions/dimensions   
plt.draw() 

# Get proper ratio here
# Computed from the last `ax` (being the current)
xmin, xmax = ax.get_xbound()
ymin, ymax = ax.get_ybound()
y2x_ratio = (ymax-ymin)/(xmax-xmin) * rows/cols

# Apply new h/w aspect ratio by changing h
# Also possible to change w using set_figwidth()
fig.set_figheight(wi * y2x_ratio)

gs.tight_layout(fig)
plt.show()

输出图:

【讨论】:

  • 我很欣赏这个答案,但是当我绘制整个地图时,我似乎无法完成这项工作(没有ax.set_extent)。这在使用更多子图(例如 4 行 x 3 列)时尤其明显。我还尝试添加fig.set_figwidth(...)x2x_ratio = 1/y2x_ratio 以及变量wi 的其他组合。这在使用 ax.set_extent 并且具有不同数量的子图的情况下有所帮助,但对于绘制整个地图似乎还不够。
  • 感谢更新。虽然这样效果更好,但它仍然会根据投影、地图的范围和子图的数量留下不同的水平/垂直间隙。我似乎也找不到好的fig.set_figwidth(...),因为改变它也会改变垂直间隙。也许我错过了什么?否则,将其适应每个情节似乎比仅使用自定义 w_padh_pad 更难。
  • @lanadaquenada 请在问题中清楚地说明您的所有要求。像这样的自定义绘图有很多细节需要处理。
  • 我编辑了这个问题,希望现在已经足够清楚了。
猜你喜欢
  • 1970-01-01
  • 2011-02-12
  • 1970-01-01
  • 2021-01-28
  • 1970-01-01
  • 2018-12-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多