设置matplotlib Axes框宽高比后如何确保Cartopy要素正常渲染?
问题
我有一个函数,可根据包含经纬度坐标的DataFrame生成地图。无论坐标对和时间戳绘制的轨迹形状如何,我希望通过axes.set_box_aspect()确保Axes框的宽高比保持不变。示例代码如下:
import cartopy.crs as ccrs import cartopy.feature as cfeature import matplotlib.pyplot as plot import pandas as pd MAP_FIXED_WIDTH = 8 MAP_FIXED_HEIGHT = 6 df = pd.DataFrame( { "timestamp": [ pd.Timestamp('2025-01-01 00:00:00 UTC'), pd.Timestamp('2025-01-01 00:30:00 UTC'), pd.Timestamp('2025-01-01 01:00:00 UTC'), pd.Timestamp('2025-01-01 01:30:00 UTC'), pd.Timestamp('2025-01-01 02:00:00 UTC'), ], "latitude": [20,22,24,26,28], "longitude": [-45,-40,-35,-30,-25], }, ) def create_map_with_scatter_points(df: pd.DataFrame) -> None: plot.figure(figsize=(MAP_FIXED_WIDTH, MAP_FIXED_HEIGHT)) axes = plot.axes( projection=ccrs.PlateCarree(central_longitude=df["longitude"].median()) ) axes.set_box_aspect(MAP_FIXED_HEIGHT / MAP_FIXED_WIDTH) axes.scatter(df["longitude"], df["latitude"], color='green') axes.set_facecolor(cfeature.COLORS["water"]) axes.add_feature(cfeature.LAND, color="lightgray") axes.add_feature(cfeature.LAKES) axes.add_feature(cfeature.RIVERS) axes.add_feature(cfeature.COASTLINE) axes.add_feature(cfeature.BORDERS, linestyle=":") axes.gridlines( draw_labels=True, linewidth=1, color="gray", alpha=0.5, linestyle="--" ) plot.show() create_map_with_scatter_points(df)
依赖版本:
matplotlib==3.10.7pandas==2.3.3cartopy==0.25.0
但我发现set_box_aspect仅控制Axes框的物理尺寸,对轴内的数据范围无影响,导致仅存在于数据范围外的Cartopy要素无法渲染(如牙买加和波多黎各的边界未显示)。使用axes.set_aspect(aspect='equal', adjustable='datalim')也得到相同结果,因为set_adjustable会忽略显式数据范围。
我希望避免通过axes.set_extent手动编写逻辑来强制框宽高比,尤其是在数据跨180度子午线、经度在±180间切换时,确保正确中心经度难度较大。请问是否有可行的解决方法?
解决方法
可以通过「先自动适配数据范围,再调整范围匹配目标宽高比」的思路实现需求,同时处理跨180度子午线的场景,无需手动计算复杂的边界逻辑:
修改后的代码示例
import cartopy.crs as ccrs import cartopy.feature as cfeature import matplotlib.pyplot as plot import pandas as pd MAP_FIXED_WIDTH = 8 MAP_FIXED_HEIGHT = 6 TARGET_ASPECT = MAP_FIXED_HEIGHT / MAP_FIXED_WIDTH df = pd.DataFrame( { "timestamp": [ pd.Timestamp('2025-01-01 00:00:00 UTC'), pd.Timestamp('2025-01-01 00:30:00 UTC'), pd.Timestamp('2025-01-01 01:00:00 UTC'), pd.Timestamp('2025-01-01 01:30:00 UTC'), pd.Timestamp('2025-01-01 02:00:00 UTC'), ], "latitude": [20,22,24,26,28], "longitude": [-45,-40,-35,-30,-25], }, ) def create_map_with_scatter_points(df: pd.DataFrame) -> None: fig = plot.figure(figsize=(MAP_FIXED_WIDTH, MAP_FIXED_HEIGHT)) central_lon = df["longitude"].median() axes = plot.axes(projection=ccrs.PlateCarree(central_longitude=central_lon)) # 先绘制数据,让Cartopy自动计算包含数据的初始范围 axes.scatter(df["longitude"], df["latitude"], color='green') # 获取初始数据范围(转换为PlateCarree投影下的坐标) x0, x1, y0, y1 = axes.get_extent(ccrs.PlateCarree()) # 计算当前范围的宽高比,对比目标值调整范围 current_width = x1 - x0 current_height = y1 - y0 current_aspect = current_height / current_width if current_aspect < TARGET_ASPECT: # 当前纬度范围过窄,扩展纬度边界以匹配目标比例 delta_y = (TARGET_ASPECT * current_width - current_height) / 2 y0 -= delta_y y1 += delta_y else: # 当前经度范围过窄,扩展经度边界以匹配目标比例 delta_x = (current_height / TARGET_ASPECT - current_width) / 2 x0 -= delta_x x1 += delta_x # 处理跨180度子午线的显示异常 if abs(central_lon) > 90: if x0 < -180: x0 += 360 if x1 > 180: x1 -= 360 # 设置调整后的范围,确保覆盖周边要素 axes.set_extent([x0, x1, y0, y1], crs=ccrs.PlateCarree()) # 固定Axes框的物理宽高比 axes.set_box_aspect(TARGET_ASPECT) # 添加地图要素 axes.set_facecolor(cfeature.COLORS["water"]) axes.add_feature(cfeature.LAND, color="lightgray") axes.add_feature(cfeature.LAKES) axes.add_feature(cfeature.RIVERS) axes.add_feature(cfeature.COASTLINE) axes.add_feature(cfeature.BORDERS, linestyle=":") axes.gridlines( draw_labels=True, linewidth=1, color="gray", alpha=0.5, linestyle="--" ) plot.show() create_map_with_scatter_points(df)
关键逻辑说明
- 先绘制数据获取初始范围,避免手动计算数据边界的繁琐操作。
- 通过对比当前范围宽高比和目标值,动态扩展纬度或经度范围,保证最终地图符合设定的物理框比例。
- 针对跨180度子午线的场景,调整经度范围的计算逻辑,避免出现地图显示断裂的问题。
- 最后设置
set_box_aspect固定Axes框的物理比例,确保无论数据范围如何,地图的显示比例一致。
内容的提问来源于stack exchange,提问作者Cstr99
相关产品推荐
相关产品推荐

