如何为GeoAxes子图创建单个统一图例
为Cartopy多子图添加统一图例
我通过循环处理6个仅时间列不同的DataFrame,生成3×2布局的Cartopy地图子图,所有地图采用相同的分类方案与颜色映射,但目前每个子图都带有独立图例,希望添加一个统一图例提升可读性。尝试过以下代码:
handles, labels = ax.get_legend_handles_labels() fig.legend(handles, labels, loc='upper center')
但收到警告:Legend does not support handles for PatchCollection instances,同时不清楚如何将图例的分箱、字体等设置整合到循环外。
数据预处理代码
import geopandas as gpd import pandas as pd # 读取全球边界shapefile world = gpd.read_file("TM_WORLD_BORDERS-0.3.shp") # 读取样本数据 df = pd.read_csv("fifsixseventeen.csv", sep=";") # 按年份拆分数据 fifteen = df[(df['date'] == 2015)].reset_index(drop=True) sixteen = df[(df['date'] == 2016)].reset_index(drop=True) seventeen = df[(df['date'] == 2017)].reset_index(drop=True) # 合并ISO编码与地理数据的函数 def merge_isocodes(df): allmentions = df.groupby("iso3")['mentions'].sum().sort_values(ascending=False).reset_index() mentionsgdf = pd.merge(allmentions, world, left_on=allmentions["iso3"], right_on=world["ISO3"], how="right").drop(columns="key_0") mentionsgdf = gpd.GeoDataFrame(mentionsgdf, geometry='geometry') return mentionsgdf onefive = merge_isocodes(fifteen) onesix = merge_isocodes(sixteen) oneseven = merge_isocodes(seventeen) # 存储各年份数据框的列表 dataframes = [onefive, onesix, oneseven]
原始绘图代码
import cartopy.crs as ccrs import cartopy.feature as cfeature import mapclassify from matplotlib.colors import rgb2hex from matplotlib.colors import ListedColormap import matplotlib.pyplot as plt plt.style.use('seaborn-v0_8-dark') # 定义Robinson投影 robinson = ccrs.Robinson() # 创建3×2子图布局 fig, axs = plt.subplots(3, 2, figsize=(12, 12), subplot_kw={'projection': robinson}) axs = axs.flatten() # 定义颜色映射与分箱数 cmap = plt.cm.get_cmap('YlOrRd', 5) # 无数据区域设置为灰色 missing_kwds = dict(color='grey', label='No Data') # 循环绘制子图 for i, df in enumerate(dataframes): mentionsgdf_robinson = df.to_crs(robinson.proj4_init) ax = axs[i] # 添加陆地与网格线 ax.add_feature(cfeature.LAND.with_scale('50m'), facecolor='lightgrey') gl = ax.gridlines(crs=ccrs.PlateCarree(), draw_labels=True, linewidth=1, color='gray', alpha=0.3, linestyle='--') gl.xlabel_style = {'fontsize': 7} gl.ylabel_style = {'fontsize': 7} # 绘制分类地图(每个子图都生成图例) mentionsgdf_robinson.plot(column='mentions', ax=ax, legend=True, cmap=cmap, legend_kwds={"loc":"center left", "title": "Number of Mentions", "prop": {"size": 7, "family": "serif"}}, missing_kwds=missing_kwds, scheme="UserDefined", classification_kwds={'bins':[20, 50, 150, 300, 510]}) # 设置子图标题 ax.set_title(f'20{i+15}', size=15, family='Serif') # 修改子图图例标签与字体 upper_bounds = mapclassify.UserDefined(mentionsgdf_robinson.mentions, bins=[20, 50, 150, 300, 510]).bins bounds = [] for index, upper_bound in enumerate(upper_bounds): lower_bound = mentionsgdf_robinson.mentions.min() if index ==0 else upper_bounds[index-1] bounds.append(f'{lower_bound:.0f} - {upper_bound:.0f}') legend = ax.get_legend() legend.get_title().set_fontsize(8) legend.get_title().set_family('serif') for bound, label in zip(bounds, legend.get_texts()): label.set_text(bound) fig.suptitle('Yearly Country Mentions in Online News about Species Threatened by Trade', fontsize=15, family='Serif') plt.tight_layout(pad=4.0) plt.show()
解决方案:手动构建统一图例
关键改动:
- 关闭子图自带图例:将绘图时的
legend=True改为legend=False,避免每个子图生成重复图例 - 提前定义统一分箱与标签:因为所有子图用相同的分类规则,直接在循环外计算图例标签
- 手动创建图例色块:用
matplotlib.patches.Patch生成对应每个分箱颜色的色块,包括无数据的灰色块 - 添加全局图例:用
fig.legend()在指定位置添加统一图例
修改后的完整绘图代码
import cartopy.crs as ccrs import cartopy.feature as cfeature import mapclassify import matplotlib.pyplot as plt from matplotlib.patches import Patch plt.style.use('seaborn-v0_8-dark') # 定义Robinson投影 robinson = ccrs.Robinson() # 创建3×2子图布局 fig, axs = plt.subplots(3, 2, figsize=(12, 12), subplot_kw={'projection': robinson}) axs = axs.flatten() # 定义颜色映射与分箱数 cmap = plt.cm.get_cmap('YlOrRd', 5) # 无数据区域设置 missing_color = 'grey' missing_label = 'No Data' # 统一的分箱边界(所有子图共用) bins = [20, 50, 150, 300, 510] # 计算图例标签(取第一个数据框的最小值作为起始) min_val = dataframes[0]['mentions'].min() legend_labels = [] for idx, upper in enumerate(bins): if idx == 0: legend_labels.append(f'{min_val:.0f} - {upper:.0f}') else: legend_labels.append(f'{bins[idx-1]:.0f} - {upper:.0f}') # 添加无数据标签 legend_labels.append(missing_label) # 提取对应颜色:分箱颜色 + 无数据灰色 legend_colors = [cmap(i) for i in range(cmap.N)] + [missing_color] # 循环绘制子图(关闭子图图例) for i, df in enumerate(dataframes): mentionsgdf_robinson = df.to_crs(robinson.proj4_init) ax = axs[i] # 添加陆地与网格线 ax.add_feature(cfeature.LAND.with_scale('50m'), facecolor='lightgrey') gl = ax.gridlines(crs=ccrs.PlateCarree(), draw_labels=True, linewidth=1, color='gray', alpha=0.3, linestyle='--') gl.xlabel_style = {'fontsize': 7} gl.ylabel_style = {'fontsize': 7} # 绘制分类地图:关闭子图图例 mentionsgdf_robinson.plot(column='mentions', ax=ax, legend=False, # 关键:关闭子图图例 cmap=cmap, missing_kwds={'color': missing_color}, scheme="UserDefined", classification_kwds={'bins': bins}) # 设置子图标题 ax.set_title(f'20{i+15}', size=15, family='Serif') # 手动构建图例元素 legend_elements = [Patch(facecolor=color, edgecolor='black', label=label) for color, label in zip(legend_colors, legend_labels)] # 添加全局统一图例 fig.legend(handles=legend_elements, loc='upper right', # 可调整位置:比如'center right' title='Number of Mentions', prop={'size': 8, 'family': 'serif'}, bbox_to_anchor=(1.15, 0.9)) # 微调位置避免遮挡 fig.suptitle('Yearly Country Mentions in Online News about Species Threatened by Trade', fontsize=15, family='Serif') plt.tight_layout(pad=4.0) plt.show()
说明:
- 所有子图共用同一套分箱规则,确保颜色映射完全一致
- 手动构建的图例元素兼容
fig.legend(),不会出现PatchCollection的报错 - 可通过
loc和bbox_to_anchor调整图例位置,比如放到子图区域外侧 - 图例的字体、标题等设置可统一在
fig.legend()的参数中调整
内容的提问来源于stack exchange,提问作者redleesy
相关产品推荐
相关产品推荐

