目录24

文章Matplotlib第 30 章

轴框、网格、尺度与标题

Matplotlib · 第 30 / 43 章

spines:轴框线

spines 是包围绘图区的那四条线(不是刻度、不是轴标签)。它们各自是 Spine 对象,可以单独隐藏、改色、改位置、截断。

Python6 行
Axes.spines["left" | "right" | "top" | "bottom"]
Spine.set_visible(False)
Spine.set_position(("outward", n) | ("axes", frac) | ("data", val) | "center" | "zero")
Spine.set_bounds(low, high)      # 只画这一段
Spine.set_color(c) / set_linewidth(w) / set_linestyle(ls)
ax.spines[:]                     # 切片:一次处理四条(3.11 正式支持,3.10 实测可用)
参数 一句话 默认值的影响
set_visible(False) 去掉这一条 默认四条全画
set_position(('outward', n)) 往外挪 n 点 默认 ('outward', 0)
set_position('zero') 挪到数据 0 处 适合「过原点坐标轴」
set_bounds(low, high) 只画这一段 默认整条
set_color / set_linewidth 颜色 / 粗细 默认黑、0.8pt

最小示例

Python10 行
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ax.plot(np.arange(6), np.arange(6) ** 1.5, 'o-')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
print(sorted(ax.spines.keys()))
# 输出: ['bottom', 'left', 'right', 'top']
plt.show()

spines 的截断与外移;grid 的主次线

set_position 的四种位置

Python11 行
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.plot([1, 2, 3])
ax.spines['left'].set_position(('outward', 10))   # 离框 10pt
print(ax.spines['left'].get_position())
# 输出: ('outward', 10)

ax.spines['bottom'].set_position('zero')          # 挪到 y=0
print(ax.spines['bottom'].get_position())
# 输出: zero

四种取值:

写法 含义
('outward', n) 沿法线往外挪 n 点
('axes', frac) 放在框内 0~1 的比例位置
('data', val) 放在数据坐标 val 处
'center' / 'zero' 快捷写法,等于 ('data', 中点) / ('data', 0)

默认值是 ('outward', 0)。('data', 0) 就是「过原点的坐标轴」,做「四象限」图必用。

set_bounds:截断

Python9 行
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ax.plot(np.arange(6), np.arange(6) ** 1.5)
ax.spines['left'].set_bounds(2, 10)
print(ax.spines['left'].get_bounds())
# 输出: (2, 10)
plt.show()

set_bounds(low, high) 只画 low~high 这一段——这就是「断轴(broken axis)」的实现方式:两条 spine 各截一半,中间留白。

注意事项

  • spines 是只读字典,不能增删键,只能 set_*。
  • set_position 的 ('outward', n) 单位是点,不是数据单位。
  • set_bounds 之后对应的刻度仍按原范围显示,可能出现「刻度在但框线没有」。
  • ax.spines[:] 在 3.10 实测可用,但官方文档把它标注为 3.11 正式支持,跨版本写 for s in ax.spines.values() 更保险。
  • 隐藏 spine 不影响刻度:spines['bottom'].set_visible(False) 后下方刻度还在(这正是 hide-params 图里最后一格的样子)。

网格线 grid()

网格线默认跟随主刻度,且默认就在数据下层。要次网格、要某个方向、要不同样式,都得显式指定 which 和 axis。

Python1 行
Axes.grid(visible=None, which="major", axis="both", **kwargs)
参数 取值 说明
visible True / False / None None 是切换
which 'major' / 'minor' / 'both' 默认只管主刻度
axis 'x' / 'y' / 'both' 默认两个方向
color / ls / lw / alpha 同 Line2D 样式

最小示例

Python10 行
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 4 * np.pi, 200)
fig, ax = plt.subplots()
ax.plot(x, np.sin(x))
ax.minorticks_on()
ax.grid(which='major', color='0.55', ls='-', lw=.8, alpha=.7)
ax.grid(which='minor', color='0.85', ls=':', lw=.6)
plt.show()

主网格实线深灰、次网格点线浅灰——这是「网格存在感足够但不抢戏」的标准做法。

只给一个方向

Python10 行
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ax.plot(np.arange(6), np.arange(6) ** 1.5, 'o-')
ax.grid(axis='y', color='0.6', ls='--', alpha=.6)
print(len(ax.get_xgridlines()), len(ax.get_ygridlines()))
# 输出: 8 8
print(ax.get_xgridlines()[0].get_visible(), ax.get_ygridlines()[0].get_visible())
# 输出: False True

注意 len() 两边都是 8——网格线对象一直都在,axis= 只是把另一方向的 visible 设成 False,并不是不创建。所以想判断「哪个方向有网格」不能数个数,要看 get_visible()。

只画横向网格是折线图的常见配置:y 方向辅助读数,x 方向不画以免和折线打架。

网格压在数据下面

Python12 行
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ax.bar(['A', 'B', 'C'], [3, 5, 4])
ax.grid(True, axis='y')
print(ax.get_axisbelow())
# 输出: line                  ← 默认值,字符串 'line'
ax.set_axisbelow(True)          # 柱子在网格之上
print(ax.get_axisbelow())
# 输出: True                  ← 显式设过就变成布尔
plt.show()

返回值有三种形态,别被 True / 'line' 混着搞晕:默认是字符串 'line'(网格在折线下面、在 patch 上面),set_axisbelow(True) 之后读出来就是布尔 True,set_axisbelow('stamp') 读出来是 'stamp'。True 和 'line' 表达的是同一个层级,只是设置方式不同。

不设 set_axisbelow(True) 时网格会画在柱子上面,视觉上像柱子被划了几道。

注意事项

  • grid() 默认只管 which='major',次网格要显式 which='minor' 且先 minorticks_on()。
  • grid() 不传 visible 是切换当前状态,传 True / False 才是设定。
  • set_axisbelow(True) 只压到线下面;柱状图的 patch 仍会盖住网格,要彻底在下面用 'stamp'。
  • 网格线的条数等于对应方向的刻度条数,set_xticks([]) 后网格自动消失。

对数轴与自定义 scale

set_xscale 换的是刻度系统(Locator + Formatter 一起换),不是「把数据取对数再画」—— 数据本身不变,只是轴被压缩了。

Python4 行
Axes.set_xscale(value, **kwargs)
Axes.set_yscale(value, **kwargs)
Axes.set_xscale("log", base=10, subs=(1.0,), nonpositive="clip")
Axes.loglog() / semilogx() / semilogy()     # 一次性设 x/y
value 含义 常用参数
'linear' 线性(默认) —
'log' 对数 base=10、subs、nonpositive
'symlog' 0 附近线性、远处对数 linthresh、linscale
'logit' logit 变换 数据必须在 (0, 1)
'function' 自定义正反函数 functions=(fwd, inv)
'functionlog' 自定义 + 对数刻度 同上

最小示例

Python11 行
import matplotlib.pyplot as plt
import numpy as np

x = np.logspace(0, 4, 200)
fig, ax = plt.subplots()
ax.plot(x, x ** 2)
ax.set_xscale('log')
ax.set_yscale('log')
print(np.round(ax.get_xticks(), 2)[:4])
# 输出: [ 0.01  0.1   1.   10.  ]
plt.show()

loglog / base=2 / symlog 三种尺度

base:换底

Python10 行
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ax.plot(np.logspace(0, 4, 200), np.logspace(0, 4, 200))
ax.set_xscale('log', base=2)
fig.canvas.draw()
print(np.round(ax.get_xticks(), 2)[:5])
# 输出: [ 0.12  0.5   2.    8.   32.  ]
plt.show()

base=2 时刻度落在 2 的幂上,标签是 2^1、2^3 这种(由 LogFormatterSciNotation 决定)。做「翻倍关系」的图(内存、文件大小)常用。

nonpositive:0 和负值怎么办

Python9 行
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.plot([0, 1, 10, 100], [1, 2, 3, 4])
ax.set_xscale('log')
try:
    fig.canvas.draw()
except Exception as e:
    print(type(e).__name__, str(e)[:80])

含 0 或负值时 log 会把它们掩码掉(图上是空白),不抛异常。要「夹到最小正值」就传 nonpositive='clip';要「当无效值丢掉的发声提示」用默认 'mask'。

symlog:正负都有

Python11 行
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
xs = np.linspace(-100, 100, 400)
ax.plot(xs, xs)
ax.set_xscale('symlog', linthresh=10)
fig.canvas.draw()
print(len(ax.get_xticks()), np.round(ax.get_xticks(), 1))
# 输出: 5 [-100.  -10.    0.   10.  100.]
plt.show()

linthresh 是线性区的半宽(|x| < linthresh 走线性)。设 linthresh=10 表示「-10~10 之间保持正常间距」。

自定义 scale:'function'

Python11 行
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
xs = np.linspace(0, 10, 200)
ax.plot(xs, xs ** 2)
ax.set_yscale('function', functions=(np.sqrt, np.square))
fig.canvas.draw()
print(type(ax.yaxis.get_scale()).__name__)
# 输出: str
plt.show()

functions=(正向, 反向) 必须互为反函数,否则刻度和数据会错位。这是做「平方根轴」「倒数轴」的通用手段。

注意事项

  • set_yscale('log') 会把 ≤0 的值掩码成空白,不报错;图上白块先怀疑它。
  • 对数轴的次刻度默认打开(subs),minorticks_off() 关不掉它。
  • base 只影响刻度位置,不改变数据映射。
  • 'logit' 要求数据严格在 (0, 1) 内,否则 UserWarning: Data has no values between 0 and 1。
  • 'function' 必须给一对互逆函数,np.log / np.exp 这种配错会静默出错。
  • loglog() / semilogx() / semilogy() 只是语法糖,等价于两次 set_*scale('log')。

标题与轴标签

标题和轴标签都是 Text 对象,挂在固定位置,但可以改对齐方式、加 padding、给 fontdict。多子图时还要区分「子图标题」和「整幅标题」。

Python5 行
Axes.set_title(label, fontdict=None, loc="center", pad=6, **kwargs)
Axes.set_xlabel(xlabel, fontdict=None, labelpad=None, *, loc=None, **kwargs)
Axes.set_ylabel(ylabel, fontdict=None, labelpad=None, *, loc=None, **kwargs)
Figure.suptitle(t, x=0.5, y=0.98, ha="center", **kwargs)   # 整幅大标题
Figure.supxlabel(t) / Figure.supylabel(t)
参数 一句话 默认值的影响
loc 'left' / 'center' / 'right' 'center',长标题不贴边
pad / labelpad 与轴的距离(点) 6 / 由 mpl 算
fontsize / fontweight / color 文字样式 跟 rcParams
suptitle 整幅标题 多子图时不要用 set_title 当总标题

最小示例

Python9 行
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
ax.plot(np.arange(5))
ax.set_title('销售额趋势', fontsize=11, pad=12)
ax.set_xlabel('月份', labelpad=8)
ax.set_ylabel('万元')
plt.show()

loc:三种对齐

Python14 行
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.plot([1, 2, 3])
ax.set_title('左对齐标题', loc='left')
print(ax.title.get_position())
# 输出: (0.5, 1.0)            ← ax.title 永远是居中那个
print(ax._left_title.get_position())
# 输出: (0.0, 1.0)            ← 左标题是另一个 Text
print(ax.get_title(loc='left'))
# 输出: 左对齐标题
ax.set_title('居中标题', loc='center')
print(ax.get_title(), '|', ax.get_title(loc='left'))
# 输出: 居中标题 | 左对齐标题      ← 两者互不覆盖

这是 loc 最容易踩的坑:loc='left' 并不会把 ax.title 挪到左边,而是写进 ax._left_title 这个独立的 Text;loc='right' 写进 ax._right_title。所以写 ax.title.get_position() 永远读到 (0.5, 1.0)——要用 ax.get_title(loc='left') 读文字、ax._left_title.get_position() 读位置。

loc='left' 让标题贴左边框——收益是排版更整齐(尤其多个子图并排时),代价是和 y 轴标签可能撞上,得配合 pad。

set_xlabel(loc='right') 让 x 轴标签贴右端,配合「单位放右边」的习惯。

多行标题

Python6 行
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.plot([1, 2, 3])
ax.set_title('主标题\n副标题(小字说明)', fontsize=10, linespacing=1.6)
plt.show()

\n 换行后行距由 linespacing 控制(默认 1.2)。多行标题会把整个绘图区往下挤,pad 要相应调小。

suptitle:整幅标题

Python10 行
import matplotlib.pyplot as plt
import numpy as np

fig, axes = plt.subplots(1, 2, figsize=(9, 3))
for i, ax in enumerate(axes):
    ax.plot(np.arange(5), np.arange(5) ** (i + 1))
    ax.set_title(f'子图 {i + 1}', fontsize=9)
fig.suptitle('2026 年分季度营收', fontsize=12, y=1.02)
plt.tight_layout()
plt.show()

suptitle 挂在 Figure 上,坐标是整幅画布的比例(y=1.02 就是画布上方一点点)。tight_layout() 默认不为 suptitle 留空间,所以要自己给 y。

注意事项

  • set_title 返回 Text 对象,想事后改文字用返回值或 ax.title.set_text()。
  • loc='left' 的标题会贴到框左边缘,长标题可能压住 y 轴标签,需要加 pad。
  • suptitle 的坐标是画布比例,set_title 的是轴比例——两者单位不同。
  • tight_layout() 不认 suptitle,要么手给 y,要么改用 constrained_layout。
  • labelpad 的默认值由字体大小和刻度长度共同决定,设成 0 常常会和刻度文字重叠。
  • 中文标题要设 rcParams['font.sans-serif'],否则出现方块(见 rcParams 一节)。

综合示例:三个子图的完整排版

Python41 行
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import numpy as np

fig, axes = plt.subplots(1, 3, figsize=(13, 3.6))
x = np.arange(12)
rng = np.random.default_rng(0)

# ① 折线:极简坐标轴(去掉上右框线 + 只留横向网格)
ax = axes[0]
ax.plot(x, rng.normal(size=12).cumsum(), 'o-', color='#4C72B0', ms=4)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.grid(axis='y', color='0.85', lw=.8)
ax.set_axisbelow(True)
ax.set_title('月度累计', loc='left', fontsize=10)
ax.set_xlabel('月', labelpad=6, fontsize=9)
ax.yaxis.set_major_locator(mticker.MaxNLocator(4))

# ② 百分比柱:PercentFormatter + 顶边刻度
ax = axes[1]
ax.bar(['Q1', 'Q2', 'Q3', 'Q4'], [18, 25, 31, 26], color='#DD8452')
ax.yaxis.set_major_formatter(mticker.PercentFormatter(xmax=100, decimals=0))
ax.yaxis.set_major_locator(mticker.MultipleLocator(10))
ax.tick_params(axis='x', direction='in', length=0, pad=6)
ax.tick_params(axis='y', direction='in', colors='0.35', labelsize=8)
ax.set_ylim(0, 40)
ax.set_title('分季度占比', loc='left', fontsize=10)

# ③ 对数轴:base=2 + 次网格
ax = axes[2]
ax.plot(np.logspace(0, 4, 60), np.logspace(0, 4, 60) ** 1.2, color='#2ca02c')
ax.set_xscale('log', base=2)
ax.set_yscale('log')
ax.grid(which='major', color='0.6', lw=.8)
ax.grid(which='minor', color='0.88', ls=':', lw=.6)
ax.set_title('翻倍关系', loc='left', fontsize=10)

fig.suptitle('三个子图的轴设置对照', fontsize=11, y=1.03)
plt.tight_layout()
plt.show()

三格分别演示三组最常用的轴配置:① 极简风(去框线 + 单向网格 + MaxNLocator),② 百分比(PercentFormatter + 刻度朝内),③ 对数(base=2 + 主次网格)。loc='left' 统一加在 set_title 上,让三个标题左边缘对齐。