首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Cartopy 绘制扇形地图,并正确添加经纬度刻度

Cartopy 绘制扇形地图,并正确添加经纬度刻度

作者头像
用户11172986
发布2026-07-27 19:56:49
发布2026-07-27 19:56:49
60
举报
文章被收录于专栏:气python风雨气python风雨

Cartopy 绘制扇形地图,并正确添加经纬度刻度

本教程以 Lambert Conformal 投影为例,绘制在文献中常见的扇形地图。

就是上期冷锋降水的锋面图,参考文献的图是长这样的

起初小编以为这只是比较特别的投影,后面研究发现压根不是一回事

后面还是搜到了云台书史的教程,https://mp.weixin.qq.com/s/q69owfKQXsYcCX__uZjIsQ

解决了怎么画扇形的问题。但是后面对经纬度刻度怎么用最少的代码画上去就犯了难。

那么下面就记录一下小编迭代了哪些不同的绘制方式。

思路

  1. PlateCarree(经纬度)坐标系中仅采样南、北两条纬线,并由路径闭合操作形成东西两条经线边界。
  2. ax.set_boundary() 将该路径设为 GeoAxes 的边框。
  3. 对每个经纬度刻度,取其与边框相交的经纬度点(如南边界为 (经度, 南纬度))。
  4. 让 Cartopy 的 Gridliner 自动求取网格线与扇形边框的交点,并以少量 xpadding/ypadding 将标签贴边放置。

这样在改变图幅大小、子图位置或投影后,刻度仍会紧贴扇形边框。

代码语言:javascript
复制
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.path as mpath
import cartopy.crs as ccrs
import cartopy.feature as cfeature
from matplotlib.font_manager import FontProperties

plt.rcParams.update({
    'font.family': 'serif',
    'font.serif': ['Nimbus Roman', 'DejaVu Serif', 'SimHei', 'WenQuanYi Micro Hei'],
    'font.size': 13,
    'figure.dpi': 200,
    'savefig.dpi': 300,
    'axes.unicode_minus': False,
    'axes.linewidth': 0.8,
})

# 地图显示范围:西、东、南、北
extent = [70, 140, 10, 60]
lonmin, lonmax, latmin, latmax = extent
data_crs = ccrs.PlateCarree()
map_crs = ccrs.LambertConformal(
    central_longitude=105, standard_parallels=(30, 60)
)

1. 构造经纬线围成的扇形边界

Lambert 投影下,固定纬度线通常为弧线,固定经度线通常为直线。只需采样南、北两条纬线,再用 CLOSEPOLY 闭合路径,即可得到扇形边界;比四边都采样更简洁。

代码语言:javascript
复制
def make_sector_path(extent, step=1):
    """返回由两条纬线和两条经线围成的地理坐标路径。"""
    west, east, south, north = extent
    lons = np.arange(west, east + step, step)

    vertices = np.vstack([
        np.c_[lons, np.full_like(lons, south)],       # 南侧纬线
        np.c_[lons[::-1], np.full_like(lons, north)], # 北侧纬线
        [west, south],                                # 闭合到起点
    ])
    codes = ([mpath.Path.MOVETO]
             + [mpath.Path.LINETO] * (len(vertices) - 2)
             + [mpath.Path.CLOSEPOLY])
    return mpath.Path(vertices, codes)

sector_path = make_sector_path(extent)

边界写法二:四条边全部采样并预先投影

这是教程早期采用的写法:将四条边全部采样,再把顶点预先转换到目标投影。它更显式,适合边界并非规则经纬线的情况;对于普通经纬度矩形,上一种两条纬线加 CLOSEPOLY 的写法更短。

代码语言:javascript
复制
def make_projected_sector_path(extent, projection, source_crs, n_points=160):
    west, east, south, north = extent
    edge_lons = np.linspace(west, east, n_points)
    edge_lats = np.linspace(south, north, n_points)
    boundary_lons = np.concatenate([
        edge_lons, np.full(n_points, east),
        edge_lons[::-1], np.full(n_points, west),
    ])
    boundary_lats = np.concatenate([
        np.full(n_points, north), edge_lats,
        np.full(n_points, south), edge_lats[::-1],
    ])
    xy = projection.transform_points(source_crs, boundary_lons, boundary_lats)
    return mpath.Path(xy[:, :2], closed=True)

# 使用时:ax.set_boundary(make_projected_sector_path(...), transform=map_crs)

2. 方法一(推荐):使用 Cartopy Gridliner

更简洁的方案是直接使用 Cartopy 的 Gridliner。它会计算网格线与 ax.spines['geo'](即 set_boundary 设置的扇形边框)的交点,所以无需手写 ax.text()、转换坐标或计算边框法线。 指定 后,经度标签显示在扇形图的南侧边界,纬度标签显示在西侧边界及弧形边框上。

代码语言:javascript
复制
# 方法一完整代码:Cartopy Gridliner(推荐)
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.path as mpath
import cartopy.crs as ccrs
import cartopy.feature as cfeature
from matplotlib.font_manager import FontProperties

extent = [70, 140, 10, 60]
lonmin, lonmax, latmin, latmax = extent
data_crs = ccrs.PlateCarree()
map_crs = ccrs.LambertConformal(central_longitude=105, central_latitude=35)
lon_ticks = np.arange(lonmin, lonmax + 1, 10)
lat_ticks = np.arange(latmin, latmax + 1, 10)

lons = np.arange(lonmin, lonmax + 1, 1)
vertices = np.vstack([
    np.c_[lons, np.full_like(lons, latmin)],
    np.c_[lons[::-1], np.full_like(lons, latmax)],
    [lonmin, latmin],
])
codes = ([mpath.Path.MOVETO]
         + [mpath.Path.LINETO] * (len(vertices) - 2)
         + [mpath.Path.CLOSEPOLY])
boundary = mpath.Path(vertices, codes)

fig = plt.figure(figsize=(7, 4.5), dpi=200)
ax = fig.add_subplot(1, 1, 1, projection=map_crs)
ax.set_extent(extent, crs=data_crs)
ax.set_boundary(boundary, transform=data_crs)
ax.add_feature(cfeature.LAND, facecolor='#f5f5f5')
ax.add_feature(cfeature.OCEAN, facecolor='#eaf4fb')
ax.add_feature(cfeature.COASTLINE, linewidth=0.7)
ax.spines['geo'].set_linewidth(1.2)

# Gridliner:经度标签在底部,纬度标签在左侧和弧形边框
gl = ax.gridlines(
    crs=data_crs, draw_labels={'bottom': 'x', 'left': 'y', 'geo': 'y'},
    xlocs=lon_ticks, ylocs=lat_ticks,
    x_inline=False, y_inline=False, rotate_labels=False,
    xpadding=1, ypadding=1,
    color='gray', linewidth=0.5, linestyle='--', alpha=0.55,
)
gl.xlabel_style = {'size': 12}
gl.ylabel_style = {'size': 12}

ax.set_title('方法一:Cartopy Gridliner',
             fontproperties=FontProperties(family='SimHei'), pad=18)
plt.show()
image
image

image

4. 其它三种经纬度标注方法

下面保留讨论中出现过的另外三种方案。为避免重复绘图代码,先定义一个只负责创建扇形底图的函数。

代码语言:javascript
复制
def create_sector_axes(title):
    fig = plt.figure(figsize=(7, 4.5))
    ax = fig.add_subplot(1, 1, 1, projection=map_crs)
    ax.set_extent(extent, crs=data_crs)
    ax.set_boundary(sector_path, transform=data_crs)
    ax.add_feature(cfeature.LAND, facecolor='#f5f5f5')
    ax.add_feature(cfeature.OCEAN, facecolor='#eaf4fb')
    ax.add_feature(cfeature.COASTLINE, linewidth=0.7)
    ax.spines['geo'].set_linewidth(1.2)
    ax.gridlines(
        crs=data_crs, draw_labels=False,
        xlocs=lon_ticks, ylocs=lat_ticks,
        color='gray', linewidth=0.5, linestyle='--', alpha=0.55,
    )
    ax.set_title(title, fontproperties=FontProperties(family='SimHei'), pad=18)
    return fig, ax

方法二:ax.text() 加手工经纬度位置

这种写法最直观,也最接近最初示例:先准备标签,再为每个标签人工指定一个经纬度位置。优点是可以精细控制每个标签;缺点是范围、投影或画布尺寸改变后通常需要重新调坐标。

代码语言:javascript
复制
# 方法二完整代码:ax.text() + 手工经纬度位置
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.path as mpath
import cartopy.crs as ccrs
import cartopy.feature as cfeature

extent = [70, 140, 10, 60]
lonmin, lonmax, latmin, latmax = extent
data_crs = ccrs.PlateCarree()
map_crs = ccrs.LambertConformal(central_longitude=105, central_latitude=35)
lon_ticks = np.arange(lonmin, lonmax + 1, 10)
lat_ticks = np.arange(latmin, latmax + 1, 10)

lons = np.arange(lonmin, lonmax + 1, 1)
vertices = np.vstack([
    np.c_[lons, np.full_like(lons, latmin)],
    np.c_[lons[::-1], np.full_like(lons, latmax)],
    [lonmin, latmin],
])
codes = ([mpath.Path.MOVETO]
         + [mpath.Path.LINETO] * (len(vertices) - 2)
         + [mpath.Path.CLOSEPOLY])
boundary = mpath.Path(vertices, codes)

fig = plt.figure(figsize=(7, 4.5), dpi=200)
ax = fig.add_subplot(1, 1, 1, projection=map_crs)
ax.set_extent(extent, crs=data_crs)
ax.set_boundary(boundary, transform=data_crs)
ax.add_feature(cfeature.LAND, facecolor='#f5f5f5')
ax.add_feature(cfeature.OCEAN, facecolor='#eaf4fb')
ax.add_feature(cfeature.COASTLINE, linewidth=0.7)
ax.spines['geo'].set_linewidth(1.2)
ax.gridlines(
    crs=data_crs, draw_labels=False, xlocs=lon_ticks, ylocs=lat_ticks,
    color='gray', linewidth=0.5, linestyle='--', alpha=0.55,
)

labels = ([f'{tick}°N' for tick in lat_ticks]
          + [f'{tick}°E' for tick in lon_ticks])
# 这些位置仅针对当前范围和投影,改变参数后需要重新微调。
text_lons = [66, 65, 64, 63, 64, 63,
             68, 78, 87.5, 96.5, 106, 116.5, 128, 137]
text_lats = [9.5, 18.5, 28.5, 38, 48.5, 58.5,
             7, 7.25, 7, 7, 8, 7.8, 8, 8.2]
for x, y, label in zip(text_lons, text_lats, labels):
    ax.text(x, y, label, fontsize=10, transform=ccrs.Geodetic(),
            ha='center', va='center', clip_on=False)
ax.set_title('方法二:手工指定 ax.text() 位置', fontproperties=FontProperties(family='SimHei'), pad=18)
plt.show()
image
image

image

方法三:以边框地理坐标为锚点,使用固定 points 偏移

每个标签直接锚定在经纬线与边框的交点,然后只向外移动几 points。它比手工位置稳定,代码也不复杂;但偏移方向仍假设南边界向下、西边界向左。

代码语言:javascript
复制
# 方法三完整代码:边框地理坐标锚点 + 固定 points 偏移
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.path as mpath
import cartopy.crs as ccrs
import cartopy.feature as cfeature

extent = [70, 140, 10, 60]
lonmin, lonmax, latmin, latmax = extent
data_crs = ccrs.PlateCarree()
map_crs = ccrs.LambertConformal(central_longitude=105, central_latitude=35)
lon_ticks = np.arange(lonmin, lonmax + 1, 10)
lat_ticks = np.arange(latmin, latmax + 1, 10)
lons = np.arange(lonmin, lonmax + 1, 1)
vertices = np.vstack([
    np.c_[lons, np.full_like(lons, latmin)],
    np.c_[lons[::-1], np.full_like(lons, latmax)],
    [lonmin, latmin],
])
codes = ([mpath.Path.MOVETO]
         + [mpath.Path.LINETO] * (len(vertices) - 2)
         + [mpath.Path.CLOSEPOLY])
boundary = mpath.Path(vertices, codes)

def add_fixed_tick(ax, lon, lat, label, offset, ha, va):
    transform = data_crs._as_mpl_transform(ax)
    ax.annotate(
        '', xy=(lon, lat), xycoords=transform,
        xytext=offset, textcoords='offset points',
        arrowprops=dict(arrowstyle='-', lw=0.7, color='black'),
        annotation_clip=False,
    )
    ax.annotate(
        label, xy=(lon, lat), xycoords=transform,
        xytext=(offset[0] * 1.7, offset[1] * 1.7),
        textcoords='offset points', ha=ha, va=va, fontsize=10,
        annotation_clip=False,
    )

fig = plt.figure(figsize=(7, 4.5), dpi=200)
ax = fig.add_subplot(1, 1, 1, projection=map_crs)
ax.set_extent(extent, crs=data_crs)
ax.set_boundary(boundary, transform=data_crs)
ax.add_feature(cfeature.LAND, facecolor='#f5f5f5')
ax.add_feature(cfeature.OCEAN, facecolor='#eaf4fb')
ax.add_feature(cfeature.COASTLINE, linewidth=0.7)
ax.spines['geo'].set_linewidth(1.2)
ax.gridlines(
    crs=data_crs, draw_labels=False, xlocs=lon_ticks, ylocs=lat_ticks,
    color='gray', linewidth=0.5, linestyle='--', alpha=0.55,
)
for lon_tick in lon_ticks[1:]:  # 跳过左下角,避免双标签重叠
    add_fixed_tick(ax, lon_tick, latmin, f'{lon_tick}°E', (0, -3), 'center', 'top')
for lat_tick in lat_ticks:
    add_fixed_tick(ax, lonmin, lat_tick, f'{lat_tick}°N', (-3, 0), 'right', 'center')
ax.set_title('方法三:边框锚点 + 固定偏移', fontproperties=FontProperties(family='SimHei'), pad=18)
plt.show()
image
image

image

方法四:根据边框切线自动计算向外法线

该方案会在显示坐标中计算边框切线,再得到垂直于边框、朝向图外的法线。它可以画出真正垂直于弧形边框的短刻度线,适合对刻度方向要求严格的出版图;代价是辅助函数较长。

代码语言:javascript
复制
# 方法四完整代码:自动计算边框向外法线
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.path as mpath
import cartopy.crs as ccrs
import cartopy.feature as cfeature

extent = [70, 140, 10, 60]
lonmin, lonmax, latmin, latmax = extent
data_crs = ccrs.PlateCarree()
map_crs = ccrs.LambertConformal(central_longitude=105, central_latitude=35)
lon_ticks = np.arange(lonmin, lonmax + 1, 10)
lat_ticks = np.arange(latmin, latmax + 1, 10)
lons = np.arange(lonmin, lonmax + 1, 1)
vertices = np.vstack([
    np.c_[lons, np.full_like(lons, latmin)],
    np.c_[lons[::-1], np.full_like(lons, latmax)],
    [lonmin, latmin],
])
codes = ([mpath.Path.MOVETO]
         + [mpath.Path.LINETO] * (len(vertices) - 2)
         + [mpath.Path.CLOSEPOLY])
boundary = mpath.Path(vertices, codes)

def add_normal_tick(ax, xy, tangent_points, label, tick_length=3, gap=2):
    transform = data_crs._as_mpl_transform(ax)
    point = transform.transform(xy)
    p0, p1 = transform.transform(tangent_points)
    tangent = p1 - p0
    normal = np.array([-tangent[1], tangent[0]])
    normal /= np.linalg.norm(normal)

    center = transform.transform(((lonmin + lonmax) / 2, (latmin + latmax) / 2))
    if np.dot(normal, point - center) < 0:
        normal *= -1

    normal_pt = normal * 72 / ax.figure.dpi
    ha = 'left' if normal[0] > 0.25 else ('right' if normal[0] < -0.25 else 'center')
    va = 'bottom' if normal[1] > 0.25 else ('top' if normal[1] < -0.25 else 'center')
    ax.annotate(
        '', xy=xy, xycoords=transform,
        xytext=tuple(normal_pt * tick_length), textcoords='offset points',
        arrowprops=dict(arrowstyle='-', color='black', lw=0.7),
        annotation_clip=False,
    )
    ax.annotate(
        label, xy=xy, xycoords=transform,
        xytext=tuple(normal_pt * (tick_length + gap)), textcoords='offset points',
        ha=ha, va=va, fontsize=10, annotation_clip=False,
    )

fig = plt.figure(figsize=(7, 4.5), dpi=200)
ax = fig.add_subplot(1, 1, 1, projection=map_crs)
ax.set_extent(extent, crs=data_crs)
ax.set_boundary(boundary, transform=data_crs)
ax.add_feature(cfeature.LAND, facecolor='#f5f5f5')
ax.add_feature(cfeature.OCEAN, facecolor='#eaf4fb')
ax.add_feature(cfeature.COASTLINE, linewidth=0.7)
ax.spines['geo'].set_linewidth(1.2)
ax.gridlines(
    crs=data_crs, draw_labels=False, xlocs=lon_ticks, ylocs=lat_ticks,
    color='gray', linewidth=0.5, linestyle='--', alpha=0.55,
)
for lon_tick in lon_ticks[1:]:
    add_normal_tick(
        ax, (lon_tick, latmin),
        [(lon_tick - 0.3, latmin), (lon_tick + 0.3, latmin)],
        f'{lon_tick}°E',
    )
for lat_tick in lat_ticks:
    add_normal_tick(
        ax, (lonmin, lat_tick),
        [(lonmin, lat_tick - 0.3), (lonmin, lat_tick + 0.3)],
        f'{lat_tick}°N',
    )
ax.set_title('方法四:自动法线方向的刻度', fontproperties=FontProperties(family='SimHei'), pad=18)
plt.show()
image
image

image

5. 方法选择

方法

代码量

随投影和画布自动适配

可画严格垂直的短刻度线

推荐场景

Gridliner

最少

一般地图,首选

手工 ax.text()

中等

特殊版式、逐标签微调

固定偏移 annotate

中等

基本可以

近似

固定投影和边界方向

自动法线 annotate

最多

出版图、严格刻度方向

常见问题与调整

  • 标签离边框太远或太近:调整 xpaddingypadding;单位均为 points。设为 0 最贴边,负数会将标签放进图内。
  • 改了地图范围:修改 extent 后重新运行构造 sector_path 的单元格,并同步更新 lon_tickslat_ticks
  • 在多子图中使用:对每个 ax 分别调用 ax.set_boundary(sector_path, ...)ax.gridlines(...),不要再使用 fig.text()
  • 需要短刻度线Gridliner 负责边界交点和标签;若版式还要求短线,可在该交点上额外使用 ax.annotate(),但绝大多数地图只需网格线和贴边标签。
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-07-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 气python风雨 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Cartopy 绘制扇形地图,并正确添加经纬度刻度
    • 思路
    • 1. 构造经纬线围成的扇形边界
      • 边界写法二:四条边全部采样并预先投影
    • 2. 方法一(推荐):使用 Cartopy Gridliner
    • 4. 其它三种经纬度标注方法
      • 方法二:ax.text() 加手工经纬度位置
      • 方法三:以边框地理坐标为锚点,使用固定 points 偏移
      • 方法四:根据边框切线自动计算向外法线
    • 5. 方法选择
    • 常见问题与调整
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档