☰
Matplotlib图例完全指南:legend()参数、位置与样式全解析
2026/10/15 1:35:57 网站建设 项目流程

做到真正“一张图讲清一件事”的时候,图例的重要性会被瞬间放大。调色、改坐标轴、换字体这些功夫大家都会做,但最后往往用legend()的默认值草草收场,等图导出来才发现边框压着曲线、文字和线条挤成一团、散点图和折线图的符号比例也很别扭。这些问题既不致命,却又真实拉低观感。

这篇文章准备把legend()函数的参数从里到外过一遍:默认调用是怎么工作的、loc和bbox_to_anchor的位置逻辑怎么组合才不翻车、外观和字体参数怎么跟不同出图风格匹配,以及实际项目里最常见的几种翻车现场。内容不追求把文档抄一遍,而是按我自己的使用习惯,把真正能救命的参数单独拎出来讲透。

1. 先搞清楚legend()到底在管什么

很多人对legend()的认知停留在“加个框、列出变量名”这一层,这也能解释为什么大多数图的图例都像“贴上去的”。要理解legend(),先要理解它的数据来源,以及它返回的那个Legend对象到底是什么。

legend()本质上是在当前坐标轴上收集所有带有label属性的图形元素,把这些元素对应的“句柄”(handle,也就是线条、色块、散点集合等对象)和“标签文本”(label)配对,再按照给定的布局规则摆放到一个独立图层上。它不等于简单的文字注释,它生成的是一整套可以继续操作的图形对象。

1.1 无参调用到底收集了什么

最常见的写法是:

import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(8, 5)) ax.plot([1, 2, 3, 4], label="训练集") ax.plot([1, 3, 2, 4], label="验证集") ax.legend()

这里的ax.legend()没有任何参数,Matplotlib 会自动从当前Axes对象里寻找所有“带 label 的 Artist”,然后把它们生成图例。关键点在于:如果没有给曲线设置label,那legend()可能什么都收集不到,不会报错,但也只会得到一个空图例框。

另一个被经常误用的做法是给legend()直接传字符串列表:

ax.plot([1, 2, 3, 4]) ax.plot([1, 3, 2, 4]) ax.legend(["训练集", "验证集"])

这种写法也能出来图例,但背后的逻辑是“按当前位置顺序把字符串依次分配给已有图形元素”。一旦你之后调整了绘图顺序,或者在中间插入了一根不需要进图例的辅助线,字符串列表和曲线的对应关系就会错位,图例上显示的名称会张冠李戴。

我个人的建议非常明确:尽量用label加无参legend()的组合,让 Matplotlib 自己收集,避免人为维护顺序。如果手动传handles和labels,也必须保证两边的顺序严格对应。

1.2legend()管的是三层内容

要记住legend()的所有参数,不需要死记,只需要理解它管三个层面。

第一层是“内容层”,解决哪些元素进图例、元素的顺序、元素的句柄样式。涉及handles、labels、numpoints、scatterpoints、markerscale等参数。第二层是“位置层”,解决图例放在坐标系内还是坐标系外、跟哪个锚点对齐、图例项如何排列。涉及loc、bbox_to_anchor、ncol、mode等参数。第三层是“外观层”,解决边框、背景、圆角、阴影、字号等视觉细节。涉及frameon、facecolor、edgecolor、fancybox、shadow、fontsize等参数。

理解了这三层,后面不管遇到什么图例需求,都能立刻定位到对应参数,不会一上来就乱调。

2. 第一梯队:位置与锚定逻辑(loc与bbox_to_anchor)

图例放在哪,永远是第一个要解决的痛点。loc负责选择“对齐基准点”,bbox_to_anchor负责把这个基准点钉到某个具体坐标上。两者必须配套理解,只调其中一个经常会得到莫名其妙的位置。

2.1loc参数:从字符串到数字编码

loc最常见的取值是这些:

字符串数字编码含义
'best'0自动选择重叠较少的区域
'upper right'1右上角
'upper left'2左上角
'lower left'3左下角
'lower right'4右下角
'right'5右侧居中
'center left'6左侧居中
'center right'7右侧居中
'lower center'8底部居中
'upper center'9顶部居中
'center'10正中心

数字编码是从旧版本保留下来的,现在写字符串可读性更好,但偶尔会看到老代码用loc=1这种写法,知道含义即可。

这里有个容易被忽略的点:'best'并不是真正全局最优,它只是通过一个启发式算法去寻找“重叠面积较小”的区域。算法本身不差,但它不会考虑你的业务重点。比如你的曲线右上方是数据密集区,左下方反而是空白,算法可能因为“总重叠面积小”选到右上方,结果把关键曲线挡掉了。

所以实际项目里,不要过于依赖'best',尤其是当你的图例项很多、数据分布又不规则时,手动指定角落位置更可控。

2.2bbox_to_anchor:把图例放到坐标轴外面

bbox_to_anchor是legend()中最强大也最容易被误解的参数。它的核心作用是:把loc指定的那个图例“角”,钉到bbox_to_anchor指定的点上。

举一个高频场景:把图例放到坐标轴外部的右上方。

fig, ax = plt.subplots(figsize=(8, 5)) ax.plot([1, 2, 3, 4], label="方案A") ax.plot([1, 3, 2, 4], label="方案B") ax.legend(bbox_to_anchor=(1.02, 1), loc="upper left", borderaxespad=0)

这段代码的效果是:图例的左上角,对齐到坐标为(1.02, 1)的点上。这个坐标默认是相对于当前坐标轴比例的,1就代表坐标轴右上角,1.02表示在坐标轴右侧多出的 2% 宽度处。所以整体图例在坐标轴外侧,且靠着右上角。

如果不加loc,只写bbox_to_anchor=(1.02, 1),图例的哪个角来对齐这个点并不明确,结果容易漂移。所以loc指定“图例自己的哪个角”,bbox_to_anchor指定“角要贴到哪个位置”,两者是绑定的。

当图例在坐标轴外时,还要注意保存导出问题。直接savefig()会因为图例超出画布而被裁掉,最省事的处理方式是:

fig.savefig("result.png", bbox_inches="tight", dpi=300)

或者预先给右侧留出空间:

fig.subplots_adjust(right=0.75)

如果是普通展示,不导出,那用tight_layout有时候也能自动调整,但可靠程度不如subplots_adjust。

2.3 四元组加上mode="expand":制造横跨整行的图例

bbox_to_anchor除了传二元组,还可以传四元组,格式是(x, y, width, height)。这时候它描述的是一个虚拟矩形,典型场景是放在坐标轴上方当“顶部标题行”:

ax.legend( bbox_to_anchor=(0, 1.02, 1, 0.12), loc="lower left", mode="expand", ncol=3, borderaxespad=0 )

这里的(0, 1.02, 1, 0.12)表示:从坐标轴最左侧开始,往上 1.02 倍高度处,横向覆盖整个宽度,高度 0.12。配合loc="lower left",表示图例底部贴近这个矩形的底边;mode="expand"则让图例在水平方向拉伸,把整块宽度占满。

这个组合常用于三个图例项横排成一行,像小标题一样挂在图上方,比默认挤在右上角清爽得多。

3. 第二梯队:外观与样式参数(边框、阴影、圆角、透明度)

位置定好了,接着就是让图例和整张图的风格统一。这里的参数数量不少,但都直白,掌握核心组合就能应付大多数场景。

3.1 边框、圆角、阴影、透明度的完整配合

默认图例带一个浅色底框,但这个框放在出版图或者比赛图上往往显得突兀。常见的微调方式:

leg = ax.legend( frameon=True, facecolor="white", edgecolor="#333333", framealpha=0.9, fancybox=False, shadow=False )
  • frameon=False:不要框,完全融入画布。适合底色很干净的图。
  • facecolor="none":框透明,只留边界线。
  • framealpha=0.9:背景透明度,0 为全透明,1 为完全不透明。建议不设成 1,留一点透气感。
  • fancybox=True:圆角边框,默认是闭合的,更适合偏活泼风格。
  • shadow=True:给图例加投影,视觉上像浮起来,但这种效果在现代风格里很容易显得脏。

经验之谈:科研论文的图,推荐fancybox=False加细边框;做演示PPT或者业务报表,可以开圆角,但阴影最好别乱用。

3.2 字号与字体调整的两个入口

控制图例文字的常见参数是fontsize,也有prop这种更底层的写法:

ax.legend(fontsize=12) # 等价写法 ax.legend(prop={"size": 12})

fontsize还可以传描述性字符串,比如"small"、"medium"、"large",分别对应一组预设字号。

但中文场景下,光调字号不够,字体族经常要一起指定。Windows 下的宋体、黑体,macOS 下的苹方,名字各不相同。如果不统一,Matplotlib 会回退到默认字体,中文全部变成方框。推荐在画图前统一配置:

plt.rcParams["font.sans-serif"] = ["Microsoft YaHei", "SimHei", "Arial"] plt.rcParams["axes.unicode_minus"] = False

第二行的axes.unicode_minus是必加项,否则坐标轴的负号会变成乱码方块。字体族在prop里也可以单独设置,适合只需要图例换字体的场景。

3.3 用Patch和Line2D自己制造图例样式

有时候数据本身并不需要画出真实图形,只是想要一个“说明色块的图例”。比如你画的是柱状图,希望图例里显示背景说明色块,而不是柱体本身。这时候可以手工制造样本:

from matplotlib.patches import Patch legend_elements = [ Patch(facecolor="#2E86AB", label="类别A"), Patch(facecolor="#A23B72", label="类别B"), ] ax.legend(handles=legend_elements)

折线类型也可以用类似方式:

from matplotlib.lines import Line2D legend_elements = [ Line2D([0], [0], color="#2E86AB", lw=2, label="方案A"), Line2D([0], [0], color="#A23B72", lw=2, linestyle="--", label="方案B"), ] ax.legend(handles=legend_elements)

这种手工构造的方案在论文插图里很实用,尤其是当图形本身太复杂,直接用真实句柄会连带着把无关样式也带进图例。

4. 第三梯队:图例内容控制(句柄、标签、筛选与重排)

图例内容不是“画了哪些曲线就显示哪些曲线”这么简单。实际项目中经常要手动控制哪些进图例、哪些不进,以及进图例后的排列顺序。

4.1handles和labels:手动指定时的顺序陷阱

手动传参的完整写法:

line1, = ax.plot([1, 2, 3, 4], label="训练集") line2, = ax.plot([1, 3, 2, 4], label="验证集") ax.legend(handles=[line1, line2], labels=["训练集", "验证集"])

这里有个新手常踩的坑:ax.plot()返回的是一个Line2D列表,即使画一条线,返回的也是一个长度为 1 的列表。如果不加逗号解包,直接写line1 = ax.plot(...),那line1就变成列表了,传给handles时会出现奇怪的嵌套问题。正确写法就是上面那样加逗号line1, = ax.plot(...)。

当然,实际项目里往往不需要手动取变量,可以用get_legend_handles_labels()一次性取回:

handles, labels = ax.get_legend_handles_labels() ax.legend(handles, labels)

这个方法返回两个列表,顺序与当前图例顺序一致,可以方便地对它们做切片、倒序、合并。

4.2 用get_legend_handles_labels()做筛选和合并

多子图场景下特别有用。假设一个画布里有四个子图,你希望把四个子图的图例统一收集到整个画布的最上方,而不是每个子图各画一个。合并写法:

all_handles = [] all_labels = [] for a in axes: handles, labels = a.get_legend_handles_labels() all_handles.extend(handles) all_labels.extend(labels) fig.legend(all_handles, all_labels, loc="upper center", ncol=4, bbox_to_anchor=(0.5, 1.02))

注意,如果多个子图中的对象有重复标签,这样直接extend会造成图例里出现重复项。需要在合并前做一次去重,或者用字典以 label 为键去重。

筛选也一样。只想要前两个对象的图例,直接切片:

handles, labels = ax.get_legend_handles_labels() ax.legend(handles[:2], labels[:2])

这个逻辑比在绘图时控制label更灵活,因为它不要求你提前决定哪些曲线要显示。

4.3 用_nolegend_标记隐藏曲线

如果不希望某条线出现在图例里,又不想改动整套handles筛选逻辑,最省事的办法是给这条线设置特殊标签:

ax.plot(x, noise, label="_nolegend_")

只要 label 是以下划线开头的字符串,Matplotlib 就会默认忽略这条线,不把它收进图例。这是个约定俗成的机制,比“不设置label”更显式,因为你知道这条线原本有名字,只是不想展示。

另一个隐蔽技巧是给曲线手动设置set_label(""),空字符串在某些版本的 Matplotlib 中会被当作“没有标签”处理,但可靠程度不如_nolegend_,所以建议统一使用下划线开头的方式。

4.4 散点、折线混编时的符号控制

当图例里既有折线又有散点时,符号大小和数量需要单独调,否则可能出现折线图例里显示两个点密密麻麻,而散点图例则点太小看不清的情况。

  • numpoints:控制折线图例中显示的标记点数,默认是 2,一般设成 1 更干净。
  • scatterpoints:控制散点图图例中点标的个数,默认也是 1,除非想表达多个点,一般 1 就够。
  • markerscale:按比例放大或缩小图例里的标记尺寸,数值大于 1 会放大。
ax.legend(numpoints=1, scatterpoints=1, markerscale=1.2)

我实际出图时,markerscale经常设成 1.2 到 1.5,因为图例里的符号总体偏小,放大会让读者看得更清楚,也不会跟正文文本抢视觉。

5. 第四梯队:空隙、列数与排版细节

当图例项很多、或者图例需要放在横向空间有限的位置时,单纯调到ncol还不够,周围的间隙参数才是决定排版质感的关键。这类参数单个看都不起眼,组合起来效果立竿见影。

5.1ncol与columnspacing

ncol控制图例列数。比如你要让四个图例项排成两排,就设ncol=2;要横排成一行,就设ncol=4:

ax.legend(loc="upper center", bbox_to_anchor=(0.5, 1.08), ncol=4, columnspacing=1.0)

columnspacing是列与列之间的水平间距,单位为字体大小比例。默认值是 1,在字体较大时,两列文字之间可能贴得太紧。这种情况下可以稍微调到 1.2 或 1.5。

顺带提醒,列数不是越多越好。当ncol超过 3 或 4,每个图例项的可读宽度会变小,长变量名会被截断,所以我一般只在变量名较短时用多列布局。

5.2labelspacing、handlelength、handletextpad、borderaxespad

这四个参数控制图例内部和外部的间隙:

参数控制对象默认值经验值
labelspacing条目之间的垂直间距0.50.6 到 1.0
handlelength句柄线条的长度2.01.5 到 2.5
handletextpad句柄与文字之间的水平距离0.81.0 到 1.4
borderaxespad图例框与坐标轴之间的间距0.5图例置外时设 0

我的习惯是:条目比较多时,labelspacing调大一点,避免文字行距挤成一条;线条型数据比较多时,handlelength缩短,不让那条示例线拖得太长;而图例放在坐标轴外时,borderaxespad=0可以让图例严丝合缝地靠在坐标轴边缘,不留白边。

这组间距单位都是以当前字体大小为基准的“相对值”,所以字体改大之后不需要重新调,它们会自动跟随变大。

5.3title与对齐方式

图例也可以有标题,用title参数:

ax.legend(title="指标说明", title_fontsize=10, labelspacing=0.5)

title_fontsize默认会继承图例字号,但单独显式设置更稳。标题出现后,图例的整体高度会增加,放在坐标轴外时要留足空间。

对齐方式参数alignment在较新版本的 Matplotlib 中可用,取值为"left"、"center"、"right"。它控制图例中文本块的对齐方式,对于多列图例尤其好用。

6. 实际项目里的高频组合与进阶玩法

这里整理几个我反复使用的场景组合,基本覆盖了大部分真实需求。

6.1 右侧外部图例与版面的配合

这是做机器学习模型对比时最常用的版式之一:图例放在坐标轴右侧,正文部分占满剩余空间。

fig, ax = plt.subplots(figsize=(8, 5)) ax.plot(x, y1, label="模型A") ax.plot(x, y2, label="模型B") ax.plot(x, y3, label="模型C") ax.legend(bbox_to_anchor=(1.02, 1), loc="upper left", borderaxespad=0) fig.subplots_adjust(right=0.75)

调整right=0.75的目的,是让坐标轴本身在画布中只占 75% 的宽度,剩下 25% 留给右侧图例。如果不做这一步,图例虽然画出来了,但坐标轴会延伸到图例下面,两者叠在一起。

6.2 多子图的共享图例

多子图共享一个图例时,要避免使用plt.legend(),因为它只会处理“当前坐标系”。正确的思路是先用循环把所有子图的句柄收集起来,再传给fig.legend()。合并时对handles和labels做顺序保持和去重:

unique_handles = [] unique_labels = [] for a in axes: handles, labels = a.get_legend_handles_labels() for h, l in zip(handles, labels): if l not in unique_labels: unique_handles.append(h) unique_labels.append(l) fig.legend(unique_handles, unique_labels, loc="upper center", ncol=4)

这样图例不会出现同一名字的重复项,顺序也跟随第一次出现的顺序。

6.3 让图例可以拖动、移除

legend()返回的Legend对象是可交互的。如果做数据探索性分析,希望手动拖动图例位置,只需:

leg = ax.legend() leg.set_draggable(True)

这行代码之后,图例就可以通过鼠标直接拖到任何位置,非常适合在 Jupyter 里快速尝试不同位置。同样的对象也支持leg.remove(),在画布上彻底删除图例,而不需要重新绘图。

对事件绑定比较熟练的读者还可以监听图例点击事件,实现“点击图例项隐藏曲线”这类交互,这已经不是legend()参数本身能覆盖的,但确实建立在理解handles对象结构的基础上。

7. 常见问题速查与排错记录

最后把我在实际使用中踩过的坑和排查思路整理成一张表,算是一个速查工具。

现象大概率原因解决办法
调了legend()但图例空白曲线没有设置label,且没有手动传handles给曲线加label,或手动传handles
图例名称和曲线顺序对不上使用字符串列表传参,顺序错位改用label加无参legend()
导出图片时图例被裁掉图例在坐标轴外但没预留空间savefig(bbox_inches="tight")或subplots_adjust(right=0.75)
图例文字出现方框乱码中文字体未配置设置rcParams["font.sans-serif"]
图例里出现多余的线辅助线也设置了label辅助线设label="_nolegend_"
多个子图出现多个图例在每个子图里都调了legend()合并handles后统一用fig.legend()
图例覆盖了关键数据loc="best"算法选择的位置不理想手动指定loc并用bbox_to_anchor微调
折线图例里点太多numpoints未设置设numpoints=1
图例字号太小或太大fontsize未设置显式设置fontsize,不要依赖默认

这些坑大多不是“不知道参数”,而是“参数组合时机不对”。排错时我通常先判断是内容层的问题、位置层的问题,还是外观层的问题,再针对性检查对应参数,效率会高很多。


最后说一个我自己的习惯:我画完图后一定会做一步“图例检查”,把图例四个角落各放一遍,用眼睛确认遮挡情况。因为很多参数在静态代码里看不出问题,只有真正渲染出来才发现borderaxespad多留了那么一点空白,或者ncol多了一列导致整体偏宽。图例这件事,调参不难,难的是愿意花时间多看几眼渲染结果。希望这篇文章能帮你把legend()从“默认能用”推进到“顺手可控”的程度。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询