如何为带渐变色线条的误差棒图添加自定义渐变图例?
解决LineCollection渐变色线条的自定义图例问题
问题描述
我需要绘制两个带渐变色线条的误差棒图:一个线条颜色从粉色过渡到蓝色,另一个从蓝色过渡到粉色。由于plt.errorbar()无法实现渐变色线条效果,我通过LineCollection作为替代方案完成绘图:先绘制虚线样式的误差棒,再添加两段按所选colormap颜色范围着色的分段渐变色线条。但目前无法为这些渐变色线条创建自定义图例——尝试用matplotlib.lines.Line2D添加颜色块时,每个色块仅能指定单一颜色,求解决方法。
原绘图代码如下:
import numpy as np import matplotlib.pyplot as plt from matplotlib.collections import LineCollection from matplotlib.colors import BoundaryNorm, ListedColormap from matplotlib.lines import Line2D fig, axs = plt.subplots(1, 1, figsize=(10 , 8)) T = 9 x = np.linspace(0, T-1, T) data = np.random.rand(T, 30) # plot with error bars Y_mean = np.mean(data, axis=1) # mean for every row Y_std = np.std(data, axis=1, ddof=1) # std plt.errorbar(x, Y_mean, yerr=Y_std, capsize=6, elinewidth=4, ecolor = "grey",linestyle = 'dotted',color='black') # plot a line going from blue to pink y = Y_mean y_col = np.linspace(-10, 10, 8) # Create a set of line segments so that we can color them individually # This creates the points as an N x 1 x 2 array so that we can stack points # together easily to get the segments. The segments array for line collection # needs to be (numlines) x (points per line) x 2 (for x and y) points = np.array([x, y]).T.reshape(-1, 1, 2) segments = np.concatenate([points[:-1], points[1:]], axis=1) # Create a continuous norm to map from data points to colors norm_cmap = plt.Normalize(y_col.min(), y_col.max()) lc = LineCollection(segments, cmap='cool', norm=norm_cmap) # Set the values used for colormapping lc.set_array(y_col) lc.set_linewidth(4) line = axs.add_collection(lc) # plot the second plot with error bars data = np.random.rand(T, 30) + 1 Y_mean = np.mean(data, axis=1) # mean Y_std = np.std(data, axis=1, ddof=1) # std plt.errorbar(x, Y_mean, yerr=Y_std, capsize=6, elinewidth=4, ecolor = "grey",linestyle = 'dotted',color='black') # plot the second line y = Y_mean y_col = np.linspace(10, -10, 8) points = np.array([x, y]).T.reshape(-1, 1, 2) segments = np.concatenate([points[:-1], points[1:]], axis=1) norm_cmap = plt.Normalize(y_col.min(), y_col.max()) lc = LineCollection(segments, cmap='cool', norm=norm_cmap) lc.set_array(y_col) lc.set_linewidth(4) line = axs.add_collection(lc) fig.suptitle("example plot") legend_elements = [Line2D([0], [0], color='black', linestyle = 'dotted', alpha = 1, label = r'line1', lw=4), Line2D([0], [0], color='black', linestyle = 'dotted', alpha = 1, label = r'line2', lw=4)] axs.legend(handles=legend_elements, bbox_to_anchor=(1.05, 1), loc="upper left", title_fontsize = 18);
解决方案
核心思路是创建支持渐变显示的自定义图例手柄,替代只能显示单一颜色的Line2D。这里提供两种可行方案:
方案1:用LineCollection生成渐变线条手柄
通过创建小型的LineCollection模拟渐变线条,再指定对应的图例处理器让matplotlib正确渲染:
import numpy as np import matplotlib.pyplot as plt from matplotlib.collections import LineCollection from matplotlib.lines import Line2D from matplotlib.legend_handler import HandlerLineCollection fig, axs = plt.subplots(1, 1, figsize=(10 , 8)) T = 9 x = np.linspace(0, T-1, T) data = np.random.rand(T, 30) # 绘制第一个误差棒 Y_mean = np.mean(data, axis=1) Y_std = np.std(data, axis=1, ddof=1) plt.errorbar(x, Y_mean, yerr=Y_std, capsize=6, elinewidth=4, ecolor="grey", linestyle='dotted', color='black') # 绘制第一个渐变色线条(蓝→粉) y = Y_mean y_col = np.linspace(-10, 10, 8) points = np.array([x, y]).T.reshape(-1, 1, 2) segments = np.concatenate([points[:-1], points[1:]], axis=1) norm_cmap = plt.Normalize(y_col.min(), y_col.max()) lc1 = LineCollection(segments, cmap='cool', norm=norm_cmap) lc1.set_array(y_col) lc1.set_linewidth(4) axs.add_collection(lc1) # 绘制第二个误差棒 data = np.random.rand(T, 30) + 1 Y_mean = np.mean(data, axis=1) Y_std = np.std(data, axis=1, ddof=1) plt.errorbar(x, Y_mean, yerr=Y_std, capsize=6, elinewidth=4, ecolor="grey", linestyle='dotted', color='black') # 绘制第二个渐变色线条(粉→蓝) y = Y_mean y_col = np.linspace(10, -10, 8) points = np.array([x, y]).T.reshape(-1, 1, 2) segments = np.concatenate([points[:-1], points[1:]], axis=1) lc2 = LineCollection(segments, cmap='cool', norm=norm_cmap) lc2.set_array(y_col) lc2.set_linewidth(4) axs.add_collection(lc2) fig.suptitle("example plot") # 定义创建渐变线条图例手柄的函数 def create_gradient_line_handle(cmap, norm, reverse=False, lw=4): # 创建隐藏的小坐标轴用于生成渐变线段 handle_ax = fig.add_axes([0, 0, 0.1, 0.1]) handle_ax.set_visible(False) # 生成渐变线段的坐标 x_handle = np.linspace(0, 1, 10) y_handle = np.zeros_like(x_handle) points_handle = np.array([x_handle, y_handle]).T.reshape(-1, 1, 2) segments_handle = np.concatenate([points_handle[:-1], points_handle[1:]], axis=1) # 设置渐变颜色数组,reverse控制方向 color_vals = np.linspace(norm.vmin, norm.vmax, len(segments_handle)) if reverse: color_vals = color_vals[::-1] # 创建LineCollection作为手柄 lc_handle = LineCollection(segments_handle, cmap=cmap, norm=norm) lc_handle.set_array(color_vals) lc_handle.set_linewidth(lw) handle_ax.add_collection(lc_handle) return lc_handle # 创建两个渐变手柄 cmap = plt.get_cmap('cool') norm = plt.Normalize(-10, 10) handle_gradient1 = create_gradient_line_handle(cmap, norm, reverse=False) handle_gradient2 = create_gradient_line_handle(cmap, norm, reverse=True) # 组合所有图例元素 legend_elements = [ Line2D([0], [0], color='black', linestyle='dotted', lw=4, label='曲线1'), handle_gradient1, Line2D([0], [0], color='black', linestyle='dotted', lw=4, label='曲线2'), handle_gradient2 ] # 绘制图例,指定HandlerLineCollection处理渐变线条手柄 axs.legend(handles=legend_elements, handler_map={LineCollection: HandlerLineCollection(numpoints=1)}, bbox_to_anchor=(1.05, 1), loc="upper left", title_fontsize=18) plt.tight_layout() plt.show()
方案2:用AxesImage生成渐变色块手柄
通过生成小型的渐变图像作为图例手柄,实现更简洁的渐变显示:
import numpy as np import matplotlib.pyplot as plt from matplotlib.collections import LineCollection from matplotlib.lines import Line2D from matplotlib.image import AxesImage from matplotlib.legend_handler import HandlerImage fig, axs = plt.subplots(1, 1, figsize=(10 , 8)) T = 9 x = np.linspace(0, T-1, T) data = np.random.rand(T, 30) # 绘制第一个误差棒和渐变色线条 Y_mean = np.mean(data, axis=1) Y_std = np.std(data, axis=1, ddof=1) plt.errorbar(x, Y_mean, yerr=Y_std, capsize=6, elinewidth=4, ecolor="grey", linestyle='dotted', color='black') y = Y_mean y_col = np.linspace(-10, 10, 8) points = np.array([x, y]).T.reshape(-1, 1, 2) segments = np.concatenate([points[:-1], points[1:]], axis=1) norm_cmap = plt.Normalize(y_col.min(), y_col.max()) lc1 = LineCollection(segments, cmap='cool', norm=norm_cmap) lc1.set_array(y_col) lc1.set_linewidth(4) axs.add_collection(lc1) # 绘制第二个误差棒和渐变色线条 data = np.random.rand(T, 30) + 1 Y_mean = np.mean(data, axis=1) Y_std = np.std(data, axis=1, ddof=1) plt.errorbar(x, Y_mean, yerr=Y_std, capsize=6, elinewidth=4, ecolor="grey", linestyle='dotted', color='black') y = Y_mean y_col = np.linspace(10, -10, 8) points = np.array([x, y]).T.reshape(-1, 1, 2) segments = np.concatenate([points[:-1], points[1:]], axis=1) lc2 = LineCollection(segments, cmap='cool', norm=norm_cmap) lc2.set_array(y_col) lc2.set_linewidth(4) axs.add_collection(lc2) fig.suptitle("example plot") # 定义创建渐变色块手柄的函数 def create_gradient_patch_handle(cmap, norm, reverse=False, size=(100, 20)): # 生成渐变数据 gradient = np.linspace(norm.vmin, norm.vmax, size[0]) if reverse: gradient = gradient[::-1] gradient = np.tile(gradient, (size[1], 1)) # 创建AxesImage作为手柄 img_handle = AxesImage(axs, cmap=cmap, norm=norm) img_handle.set_data(gradient) return img_handle # 创建两个渐变色块手柄 cmap = plt.get_cmap('cool') norm = plt.Normalize(-10, 10) patch_gradient1 = create_gradient_patch_handle(cmap, norm, reverse=False) patch_gradient2 = create_gradient_patch_handle(cmap, norm, reverse=True) # 组合图例元素 legend_elements = [ Line2D([0], [0], color='black', linestyle='dotted', lw=4, label='曲线1'), patch_gradient1, Line2D([0], [0], color='black', linestyle='dotted', lw=4, label='曲线2'), patch_gradient2 ] # 绘制图例,指定HandlerImage处理渐变色块手柄 axs.legend(handles=legend_elements, handler_map={AxesImage: HandlerImage(stretch=1)}, bbox_to_anchor=(1.05, 1), loc="upper left", title_fontsize=18) plt.tight_layout() plt.show()
关键说明
- 两种方案都通过自定义图例手柄解决了单一颜色的限制,分别模拟渐变线条和渐变色块的显示效果
- 需要指定对应的图例处理器(
HandlerLineCollection/HandlerImage),让matplotlib能够正确渲染非标准的图例元素 - 可以根据需求调整渐变方向、线条宽度、色块尺寸等参数,匹配主图的视觉风格
内容的提问来源于stack exchange,提问作者Lidiia
相关产品推荐
相关产品推荐

