如何控制Cartopy地图投影的边距(适配多子图、色条等复杂场景)
如何控制Cartopy地图投影的边距(适配多子图、色条等复杂场景)
我太懂你被Cartopy投影边距折腾的崩溃感了——不同投影自带固定宽高比,色条、经纬标签又占了额外空间,调半天figure尺寸还是一堆多余空白,换个投影又得重来。结合你要的单/多子图兼容、精准边距控制(外边缘2.5mm、色条间距5mm)、适配各种“附加元素”的需求,给你一套能从根源解决的方案,不管你加多少经纬网标签、子图,都能稳稳控住边距。
核心思路
别再靠试错调figure尺寸了!我们用Matplotlib的Constrained Layout(比tight_layout智能10倍的布局引擎)自动适配Cartopy投影的固定宽高比,同时把所有边距、间距转换成精准的英寸数值(因为Matplotlib默认用英寸做单位),彻底告别“模糊的相对比例”和“不可控的色条宽度”。
关键步骤&修改后代码
1. 先做个单位转换工具
先写个小工具把毫米转成英寸,这样你要的2.5mm、5mm直接代入就行,不用手动算:
mm2inch = lambda x: x / 25.4 # 1英寸=25.4毫米
2. 单图场景:精准控外边缘+色条间距
修改你的单图代码,重点是用ConstrainedLayoutEngine的pad参数控制外边缘留白,用固定色条宽度+精准间距参数控制色条位置:
import cartopy.crs as ccrs import matplotlib.pyplot as plt import numpy as np from matplotlib.layout_engine import ConstrainedLayoutEngine mm2inch = lambda x: x / 25.4 def generate_perlin_noise_2d(shape, res): def f(t): return 6*t**5 - 15*t**4 + 10*t**3 delta = (res[0] / shape[0], res[1] / shape[1]) d = (shape[0] // res[0], shape[1] // res[1]) grid = np.mgrid[0:res[0]:delta[0],0:res[1]:delta[1]].transpose(1, 2, 0) % 1 angles = 2*np.pi*np.random.rand(res[0]+1, res[1]+1) gradients = np.dstack((np.cos(angles), np.sin(angles))) g00 = gradients[0:-1,0:-1].repeat(d[0], 0).repeat(d[1], 1) g10 = gradients[1:,0:-1].repeat(d[0], 0).repeat(d[1], 1) g01 = gradients[0:-1,1:].repeat(d[0], 0).repeat(d[1], 1) g11 = gradients[1:,1:].repeat(d[0], 0).repeat(d[1], 1) n00 = np.sum(grid * g00, 2) n10 = np.sum(np.dstack((grid[:,:,0]-1, grid[:,:,1])) * g10, 2) n01 = np.sum(np.dstack((grid[:,:,0], grid[:,:,1]-1)) * g01, 2) n11 = np.sum(np.dstack((grid[:,:,0]-1, grid[:,:,1]-1)) * g11, 2) t = f(grid) n0 = n00*(1-t[:,:,0]) + t[:,:,0]*n10 n1 = n01*(1-t[:,:,0]) + t[:,:,0]*n11 return np.sqrt(2)*((1-t[:,:,1])*n0 + t[:,:,1]*n1) def main(): cel_sphere = ccrs.Globe(datum=None, ellipse=None, semimajor_axis=180/np.pi, semiminor_axis=180/np.pi) sky_plate = ccrs.PlateCarree(globe=cel_sphere) ra, dec = np.mgrid[-179.5:180:1, -89.5:90:1] fake_data = generate_perlin_noise_2d(ra.shape, (1, 1)) projections = [("ee", ccrs.EqualEarth), ("mw", ccrs.Mollweide), ("lc", ccrs.LambertCylindrical)] # 你要的间距参数,直接填毫米就行 outer_margin_mm = 2.5 cbar_gap_mm = 5 outer_pad = mm2inch(outer_margin_mm) cbar_pad_rel = mm2inch(cbar_gap_mm) # 转换为绝对间距 for label, proj_cls in projections: fig, ax = plt.subplots( figsize=(20, 10), layout=ConstrainedLayoutEngine( pad=outer_pad, # 外边缘留白,精准对应2.5mm h_pad=0, w_pad=0, hspace=0, wspace=0 ), subplot_kw={ "xlim": (-180, 180), "ylim": (-90, 90), "projection": proj_cls(globe=cel_sphere), }, ) ctr = ax.contourf(ra, dec, fake_data, transform=sky_plate, cmap="Greys") # 固定色条宽度,精准控制与地图的间距 # fraction=0.05:色条占figure宽度的5%,避免尺寸不可控 # pad:用绝对间距转成的相对比例,确保不管figure多大,间距都是5mm fig.colorbar(ctr, fraction=0.05, pad=cbar_pad_rel / fig.get_figwidth()) fig.savefig(f"layout_test_{label}.png", dpi=100) plt.close(fig) print("所有单图已生成,边距符合要求!") if __name__ == "__main__": main()
3. 多子图场景:兼容经纬标签+共享色条
多子图时,只需要额外设置子图之间的间距,Constrained Layout会自动预留经纬标签的空间,完全不用手动计算:
import cartopy.crs as ccrs import matplotlib.pyplot as plt import numpy as np from matplotlib import colors from matplotlib.layout_engine import ConstrainedLayoutEngine mm2inch = lambda x: x / 25.4 def generate_perlin_noise_2d(shape, res): def f(t): return 6*t**5 - 15*t**4 + 10*t**3 delta = (res[0] / shape[0], res[1] / shape[1]) d = (shape[0] // res[0], shape[1] // res[1]) grid = np.mgrid[0:res[0]:delta[0],0:res[1]:delta[1]].transpose(1, 2, 0) % 1 angles = 2*np.pi*np.random.rand(res[0]+1, res[1]+1) gradients = np.dstack((np.cos(angles), np.sin(angles))) g00 = gradients[0:-1,0:-1].repeat(d[0], 0).repeat(d[1], 1) g10 = gradients[1:,0:-1].repeat(d[0], 0).repeat(d[1], 1) g01 = gradients[0:-1,1:].repeat(d[0], 0).repeat(d[1], 1) g11 = gradients[1:,1:].repeat(d[0], 0).repeat(d[1], 1) n00 = np.sum(grid * g00, 2) n10 = np.sum(np.dstack((grid[:,:,0]-1, grid[:,:,1])) * g10, 2) n01 = np.sum(np.dstack((grid[:,:,0], grid[:,:,1]-1)) * g01, 2) n11 = np.sum(np.dstack((grid[:,:,0]-1, grid[:,:,1]-1)) * g11, 2) t = f(grid) n0 = n00*(1-t[:,:,0]) + t[:,:,0]*n10 n1 = n01*(1-t[:,:,0]) + t[:,:,0]*n11 return np.sqrt(2)*((1-t[:,:,1])*n0 + t[:,:,1]*n1) def main(): cel_sphere = ccrs.Globe(datum=None, ellipse=None, semimajor_axis=180/np.pi, semiminor_axis=180/np.pi) sky_plate = ccrs.PlateCarree(globe=cel_sphere) ra, dec = np.mgrid[-179.5:180:1, -89.5:90:1] fake_data_1 = generate_perlin_noise_2d(ra.shape, (1, 1)) fake_data_2 = generate_perlin_noise_2d(ra.shape, (1, 1)) + 2 norm = colors.Normalize( vmin=np.min([fake_data_1, fake_data_2]), vmax=np.max([fake_data_1, fake_data_2]), ) projections = [("ee", ccrs.EqualEarth), ("mw", ccrs.Mollweide), ("lc", ccrs.LambertCylindrical)] # 统一间距参数 outer_margin_mm = 2.5 subplot_gap_mm = 3 cbar_gap_mm = 5 outer_pad = mm2inch(outer_margin_mm) subplot_pad = mm2inch(subplot_gap_mm) cbar_pad_rel = mm2inch(cbar_gap_mm) for label, proj_cls in projections: fig, (ax1, ax2) = plt.subplots( 1, 2, figsize=(20, 10), layout=ConstrainedLayoutEngine( pad=outer_pad, w_pad=subplot_pad, # 子图之间的水平间距 h_pad=0, hspace=0, wspace=0 ), subplot_kw={ "xlim": (-180, 180), "ylim": (-90, 90), "projection": proj_cls(globe=cel_sphere), }, ) # 绘制两个子图 im1 = ax1.contourf(ra, dec, fake_data_1, transform=sky_plate, cmap="Greys", norm=norm) im2 = ax2.contourf(ra, dec, fake_data_2, transform=sky_plate, cmap="Greys", norm=norm) # 添加经纬网和标签,Constrained Layout会自动预留标签空间 ax1.gridlines(draw_labels=True, crs=sky_plate) ax2.gridlines(draw_labels=True, crs=sky_plate) # 共享色条,同时控制宽度和间距 fig.colorbar(im1, ax=[ax1, ax2], fraction=0.03, pad=cbar_pad_rel / fig.get_figwidth()) fig.savefig(f"two_subplot_{label}.png", dpi=100) plt.close(fig) print("所有双子图已生成,边距符合要求!") if __name__ == "__main__": main()
避坑指南
- 别用tight_layout:它处理Cartopy固定aspect Axes的能力很差,经常会把标签切掉或者留太多空白,Constrained Layout是专门解决这种复杂布局的。
- 不要手动查投影aspect ratio:Cartopy的每个投影都会自动设置Axes的aspect为equal,Constrained Layout会自动适配这个比例,完全不用你自己算。
- 绝对数值比相对比例靠谱:用毫米转英寸的方式设置所有间距,避免用
shrink=0.5这种模糊的参数,精准控制你要的2.5mm、5mm。 - 色条宽度固定:用
fraction参数固定色条占figure的比例,避免它的尺寸不可控,导致布局乱掉。
内容来源于stack exchange
相关产品推荐
相关产品推荐

