You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何控制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()

避坑指南

  1. 别用tight_layout:它处理Cartopy固定aspect Axes的能力很差,经常会把标签切掉或者留太多空白,Constrained Layout是专门解决这种复杂布局的。
  2. 不要手动查投影aspect ratio:Cartopy的每个投影都会自动设置Axes的aspect为equal,Constrained Layout会自动适配这个比例,完全不用你自己算。
  3. 绝对数值比相对比例靠谱:用毫米转英寸的方式设置所有间距,避免用shrink=0.5这种模糊的参数,精准控制你要的2.5mm、5mm。
  4. 色条宽度固定:用fraction参数固定色条占figure的比例,避免它的尺寸不可控,导致布局乱掉。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 11:14:49