尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Python地图可视化:用matplotlib和cartopy添加专业指北针

Python地图可视化:用matplotlib和cartopy添加专业指北针 1. 项目概述为什么地图需要指北针在地理信息可视化或者任何涉及空间数据的图表中地图是核心的呈现载体。无论是分析区域销售数据、展示气象分布还是绘制旅行轨迹一张清晰的地图能让信息一目了然。然而一张“完整”的地图除了坐标轴、图例、标题这些基本要素还有一个常常被初学者甚至一些老手忽略的关键组件——指北针。你可能觉得在数字时代地图都是上北下南左西右东加个指北针是不是多此一举其实不然。首先不是所有地图都默认“北”在上方。在极地投影、某些自定义的局部坐标系或者为了美学布局而旋转了地图后方向感会立刻丢失。其次指北针是一个专业制图的标志性符号它能瞬间提升图表的专业度和可信度向读者明确传达“这是一张经过严谨处理的地理图表”。最后在一些复合图表或报告拼接中明确的指向能避免读者产生方向混淆。在Python的生态里matplotlib和cartopy是绘制科学地图的黄金组合。matplotlib提供了强大的基础绘图能力而cartopy则专门处理地理投影和海岸线、国界等地理要素的绘制。但这两个库都没有直接提供一个“开箱即用”的指北针函数。这就需要我们发挥一点创造力利用matplotlib现有的绘图元素如箭头、文本、多边形来手动构建一个既美观又实用的指北针。这个项目的核心就是解决“如何在基于matplotlib和cartopy绘制的地图上程序化地添加一个自定义的指北针”这个问题。我们将从原理到实践一步步拆解最终得到一个可以复用的函数让你以后的地图作品都拥有这个专业的点睛之笔。2. 核心思路与方案设计2.1 指北针的本质一个相对坐标系下的图形组合在动手写代码之前我们先要搞清楚指北针是什么。抛开那些花哨的设计一个基础的指北针通常包含两个部分指针一个指向正北方向通常是地图投影下的“网格北”或“真北”在简单情况下我们默认为绘图区域的Y轴正方向的箭头。标注在指针旁边标注“N”North字符有时还会有“E”、“S”、“W”。在matplotlib的坐标系里我们无法直接获取“地理北”的方向因为绘图坐标是数据坐标或图形坐标。因此我们的策略是在图形坐标系Figure Coordinates 或 Axes Coordinates中固定一个位置比如地图的右下角然后在该位置绘制一个指向“上”即图形坐标系Y轴正方向的箭头和“N”字。这意味着我们添加的指北针指示的是图形本身的“上”方。只要我们的地图没有在图形层面进行旋转这个“上”就对应着地图的“北”。这个方案简单、可靠适用于绝大多数情况。对于需要处理地图旋转或复杂投影的情况思路类似但需要将地理北方向转换到图形坐标系计算会复杂一些本项目我们先聚焦于通用场景。2.2 工具选型为何是matplotlib.patches和matplotlib.textmatplotlib的patches模块提供了大量绘制基本形状如箭头、矩形、圆形、多边形的类。FancyArrowPatch或Arrow可以用来绘制指针。而text功能则用于添加“N”标注。为什么不直接用plt.arrow()plt.arrow()是一个快捷函数但它在控制箭头样式如头部宽度、长度、尾部宽度和精确定位方面不如FancyArrowPatch灵活。FancyArrowPatch可以在图形坐标 (fig.transFigure) 或子图坐标 (ax.transAxes) 中定义位置这使得指北针可以稳定地放置在地图角落不受地图数据范围变化的影响。我们将选择在ax.transAxes坐标系下绘制。这个坐标系的原点(0,0)是子图的左下角(1,1)是右上角。这样我们可以用如(0.85, 0.1)这样的相对坐标来定位指北针无论地图的实际经纬度范围是多少指北针都会固定在子图右下角附近的位置。2.3 函数设计蓝图我们的目标是封装一个函数例如add_north_arrow(ax, x, y, size, ...)它接收一个地图子图对象ax指北针的锚点坐标(x, y)基于ax.transAxes指北针的大小size以及其他样式参数颜色、字体大小等。函数内部将完成计算指针三角形的顶点坐标。使用patches.Polygon绘制指针主体。使用patches.Rectangle绘制指针的尾部基座。使用text添加“N”标注。将所有图形元素组合并添加到子图ax中。这样用户只需要一行代码就能为地图添加上专业的指北针。3. 分步实现与代码详解接下来我们进入实战环节。请确保你已经安装了必要的库matplotlib和cartopy。如果没有安装cartopy可以通过conda install -c conda-forge cartopy或pip install cartopy来安装注意cartopy的安装可能需要一些地理数据处理库的支持如GEOS和Proj使用conda安装通常更省心。3.1 基础地图绘制首先我们创建一个带有中国区域的地图作为指北针的“画布”。import matplotlib.pyplot as plt import cartopy.crs as ccrs import cartopy.feature as cfeature from matplotlib.patches import Polygon, Rectangle # 创建图形和子图指定使用 PlateCarree 投影最常用的经纬度投影 fig plt.figure(figsize(10, 8)) ax fig.add_subplot(1, 1, 1, projectionccrs.PlateCarree()) # 设置地图显示范围大致为中国范围 ax.set_extent([70, 140, 15, 55], crsccrs.PlateCarree()) # 添加地理特征使地图更美观 ax.add_feature(cfeature.LAND, color#f0f0f0) # 陆地填充浅灰色 ax.add_feature(cfeature.OCEAN, color#d0e0ff) # 海洋填充浅蓝色 ax.add_feature(cfeature.COASTLINE, linewidth0.8) # 海岸线 ax.add_feature(cfeature.BORDERS, linestyle:, linewidth0.5) # 国界线虚线 # 添加经纬度网格 ax.gridlines(draw_labelsTrue, linewidth0.5, colorgray, alpha0.5, linestyle--) plt.title(中国区域地图示例)这段代码创建了一个显示中国区域的地图包含了陆地、海洋、海岸线、国界和网格。projectionccrs.PlateCarree()是关键它定义了地图的坐标投影系统。3.2 构建指北针绘制函数现在我们来编写核心的add_north_arrow函数。我们将设计一个比较经典的“三角形矩形基座”样式的指北针。def add_north_arrow(ax, x0.95, y0.15, size30, ratio0.8, arrow_colorblack, base_colorgray, fontsize12, fontweightbold): 在地图子图上添加一个指北针。 参数 ---------- ax : matplotlib.axes.Axes 要添加指北针的地图子图对象。 x, y : float, 默认 0.95, 0.15 指北针基座中心点在子图坐标系 (transAxes) 中的位置。 范围 [0, 1](0,0)为左下角(1,1)为右上角。 size : float, 默认 30 指北针的总高度从基座底部到箭头尖单位是点points。 ratio : float, 默认 0.8 箭头三角形部分高度与总高度 size 的比值。例如0.8表示箭头占80%高度基座占20%。 arrow_color : str, 默认 black 箭头三角形的颜色。 base_color : str, 默认 gray 基座矩形的颜色。 fontsize : int, 默认 12 “N”标注的字体大小。 fontweight : str, 默认 bold “N”标注的字体粗细。 # 将 size 从点points转换为图形坐标figure coordinates下的长度。 # 我们需要一个参考转换。一个简单的方法是使用 ax.transData 将点坐标转换为数据坐标 # 但这里我们在 transAxes 中工作。更直接的方法是我们基于 size 和 figure 的 DPI 来计算。 # 但为了简化我们假设在 transAxes 坐标系中size 是一个相对比例因子。 # 实际上为了精确控制物理尺寸计算会稍复杂。这里采用一个实用方法 # 我们用 size 作为 transAxes 坐标系下的“相对大小”通常 0.05 左右。 # 但为了让参数更直观我们内部做一次缩放。 # 定义 transAxes 下的一个参考长度 ref_length ref_length size / 1000 # 一个经验缩放因子可根据实际效果调整 # 1. 计算各部分尺寸在 transAxes 坐标系下 total_height_axes ref_length * 1.0 # 总高度 arrow_height_axes total_height_axes * ratio # 箭头高度 base_height_axes total_height_axes * (1 - ratio) # 基座高度 # 箭头宽度通常比基座窄一些看起来更协调 arrow_width_axes arrow_height_axes * 0.6 base_width_axes arrow_width_axes * 1.5 # 2. 计算顶点坐标所有坐标均在 transAxes 下 # 锚点 (x, y) 是基座底边的中心点 # 基座矩形左下角坐标 base_left x - base_width_axes / 2 base_bottom y base_rect Rectangle((base_left, base_bottom), base_width_axes, base_height_axes, transformax.transAxes, facecolorbase_color, edgecolorblack, linewidth0.8) # 箭头三角形一个等腰三角形 # 三角形底边中心点 基座顶边中心点 arrow_center_x x arrow_center_y y base_height_axes # 三角形顶点尖 tip_x arrow_center_x tip_y arrow_center_y arrow_height_axes # 三角形底边左端点 left_x arrow_center_x - arrow_width_axes / 2 left_y arrow_center_y # 三角形底边右端点 right_x arrow_center_x arrow_width_axes / 2 right_y arrow_center_y arrow_triangle Polygon([[tip_x, tip_y], [left_x, left_y], [right_x, right_y]], closedTrue, transformax.transAxes, facecolorarrow_color, edgecolorblack, linewidth0.8) # 3. 添加“N”标注 # 文本位置在箭头三角形的上方中央偏右一点避免重叠 text_x tip_x text_y tip_y ref_length * 0.2 # 在箭头尖上方一点 north_text ax.text(text_x, text_y, N, transformax.transAxes, hacenter, vabottom, # 水平居中垂直底部对齐 fontsizefontsize, fontweightfontweight, colorblack) # 4. 将图形元素添加到子图中 ax.add_patch(base_rect) ax.add_patch(arrow_triangle) # text 已经通过 ax.text 添加 # 返回添加的图形元素方便后续进行样式调整 return {base: base_rect, arrow: arrow_triangle, text: north_text}注意上述函数中size参数在transAxes坐标系下的转换是一个简化处理。在真实应用中如果你需要指北针的物理尺寸英寸或厘米固定需要进行更精确的坐标转换。这里的实现保证了指北针能随图形放大缩小而等比缩放且位置相对固定对于大多数应用已经足够。3.3 整合与测试现在将指北针添加到我们之前绘制的地图上。# 接前面的地图绘制代码 # ... # 调用函数在子图 ax 的右上角添加一个指北针 # 调整 x, y 参数可以改变位置调整 size 可以改变大小 arrow_components add_north_arrow(ax, x0.92, y0.12, size40, arrow_color#333333, base_color#666666, fontsize14) plt.tight_layout() plt.show()运行完整的代码你将得到一张带有标准指北针的中国区域地图。指北针会稳固地显示在子图的右上角附近。4. 高级定制与常见问题排查基础的指北针已经能工作了但要让它完美融入你的图表还需要一些调整和优化。4.1 样式深度定制我们的函数已经暴露了一些样式参数但你还可以通过返回的图形元素对象进行更精细的控制。# 假设 arrow_components 是上面函数返回的字典 arrow_patch arrow_components[arrow] text arrow_components[text] # 1. 修改箭头边框和填充 arrow_patch.set_edgecolor(red) arrow_patch.set_linewidth(2) arrow_patch.set_facecolor(lightblue) # 或者设置填充样式 arrow_patch.set_hatch(//) # 2. 修改文本样式 text.set_color(darkred) text.set_fontstyle(italic) text.set_backgroundcolor(yellow) # 添加文本背景色 # 3. 修改基座 base_rect arrow_components[base] base_rect.set_facecolor(none) # 透明填充 base_rect.set_edgecolor(green) base_rect.set_linestyle(--)你还可以设计更复杂的指北针样式比如罗盘式用一个圆形patches.Circle做底加上“N”、“E”、“S”、“W”四个标注。风玫瑰式结合多个箭头表示风向频率。自定义图标使用OffsetImage和AnnotationBbox插入一个指北针的PNG图标。4.2 指北针与地图投影的适配我们之前的方案假设“图形上方即地图北方”。这在PlateCarree圆柱投影等大多数常见投影且地图未旋转时成立。但如果使用了如LambertConformal兰伯特投影或Orthographic正射投影地图的“上”可能不再是地理上的“北”。此外如果你通过set_extent或数据本身限制了范围地图的“上”方向也可能发生微妙的扭曲。解决方案对于需要高精度指向的情况指北针的方向应基于地图投影进行计算。获取地理北方向在地图中心点(lon_center, lat_center)计算一个稍偏北的点如(lon_center, lat_center 0.5)。坐标转换使用地图投影的transform_points方法将这两个地理坐标点转换到地图的数据坐标(x_data, y_data)。计算角度计算数据坐标下两点连线的角度相对于X轴。旋转指北针使用matplotlib.patches的rotate方法或者直接计算旋转后的多边形顶点使指北针指向计算出的角度。这个过程涉及地理坐标到数据坐标的转换和平面角度的计算相对复杂。对于绝大多数非科研出版级的应用固定向上的指北针已经足够。如果你有强需求可以查阅cartopy官方文档中关于坐标转换的部分。4.3 常见问题与解决方案实录在实际操作中你可能会遇到以下问题问题1指北针位置跑偏或大小失控现象指北针没有出现在预期的角落或者图形缩放时指北针变得巨大或消失。排查检查坐标系确保绘制指北针所有元素Polygon,Rectangle,Text时transform参数都设置为ax.transAxes。混用ax.transData会导致位置错乱。检查坐标值x和y参数应在[0, 1]范围内。(0,0)是左下角(1,1)是右上角。(0.95, 0.1)通常是一个安全的右下角位置。调整size缩放因子如果指北针太大或太小调整函数内部ref_length size / 1000这个公式中的分母1000。这个值没有绝对标准需要根据你的图形尺寸figsize进行试验。问题2指北针被地图数据覆盖现象指北针绘制上去了但被地图上的颜色填充如陆地、海洋盖住了。解决matplotlib的图形元素有zorder属性控制绘制顺序值越大越靠上。在调用add_north_arrow函数后可以手动设置返回的Patch和Text对象的zorder。for key, patch in arrow_components.items(): if key ! text: patch.set_zorder(100) # 设置一个很高的 zorder arrow_components[text].set_zorder(101) # 文本可以更高更简单的方法是在创建Rectangle和Polygon时直接传入zorder参数。问题3保存图形时指北针模糊或变形现象屏幕显示正常但保存为 PNG/SVG/PDF 后指北针边缘出现锯齿或粗细不一致。解决提高DPI保存时使用plt.savefig(map.png, dpi300, bbox_inchestight)。高DPI能显著提升栅格格式如PNG的输出质量。使用矢量格式对于印刷或出版优先保存为 PDF 或 SVG 格式。这些是矢量格式指北针作为路径保存可以无限缩放而不失真。检查线宽指北针边框的linewidth如果设置过小如小于0.5在低DPI输出时可能渲染不佳。建议设置为0.8或1.0。问题4在tight_layout()后指北针被裁剪现象调用plt.tight_layout()自动调整布局后指北针的一部分跑到图形区域外面被裁掉了。解决tight_layout()会重新计算子图的位置和大小有时会忽略通过transAxes添加在“图边”的元素。有两种方法调整位置将指北针的(x, y)位置向子图内部移动一点比如从(0.95, 0.1)调到(0.90, 0.15)。使用constrained_layout在创建图形时使用constrained_layout它通常能更好地处理轴上的装饰物。fig plt.figure(figsize(10,8), constrained_layoutTrue)。然后可以移除tight_layout()调用。5. 封装与复用打造你的制图工具箱为了方便在多个项目中复用我们可以将这个函数和一些常用的地图样式设置封装成一个模块。创建一个名为map_utils.py的文件# map_utils.py import matplotlib.pyplot as plt import cartopy.crs as ccrs import cartopy.feature as cfeature from matplotlib.patches import Rectangle, Polygon def create_basemap(extent, figsize(12, 8), projectionccrs.PlateCarree()): 快速创建一个带有基本地理特征的地图底图。 fig plt.figure(figsizefigsize, constrained_layoutTrue) ax fig.add_subplot(1, 1, 1, projectionprojection) ax.set_extent(extent, crsccrs.PlateCarree()) # 添加常用特征 ax.add_feature(cfeature.LAND, color#f5f5f5, zorder0) ax.add_feature(cfeature.OCEAN, color#e6f2ff, zorder0) ax.add_feature(cfeature.COASTLINE, linewidth0.8, zorder1) ax.add_feature(cfeature.BORDERS, linestyle:, linewidth0.5, zorder1) ax.add_feature(cfeature.LAKES, color#e6f2ff, zorder0) ax.add_feature(cfeature.RIVERS, color#b3d9ff, linewidth0.5, zorder0) # 网格线 gl ax.gridlines(draw_labelsTrue, linewidth0.5, colorgray, alpha0.5, linestyle--) gl.top_labels False # 关闭顶部标签 gl.right_labels False # 关闭右侧标签 return fig, ax def add_north_arrow(ax, x0.95, y0.12, size40, ratio0.75, arrow_color#222222, base_color#555555, fontsize13, fontweightbold, zorder100): 将之前定义的函数完整复制过来并做小幅优化 # ... 函数内部实现同上略 ... # 注意在创建 Rectangle 和 Polygon 时加入 zorderzorder 参数 # 例如Rectangle(..., zorderzorder, ...) # 在 text 创建时也加入 zorderzorder1然后在你的主程序中可以这样优雅地调用from map_utils import create_basemap, add_north_arrow # 1. 创建底图 fig, ax create_basemap(extent[105, 125, 20, 45], figsize(10, 8)) # 可以继续添加自己的数据比如散点图、等值线等 # ax.scatter(lons, lats, transformccrs.PlateCarree(), ...) # 2. 添加指北针 add_north_arrow(ax, x0.93, y0.1, size35, arrow_colordarkblue) # 3. 添加标题、保存 ax.set_title(东亚区域地图示例, fontsize16) plt.savefig(professional_map_with_compass.png, dpi300, bbox_inchestight) plt.show()通过这样的封装你不仅解决了指北针的问题还构建了一个快速出图的工作流大大提升了制作高质量地图的效率。
返回列表