目录20

文章Matplotlib第 36 章

样式表与配色

Matplotlib · 第 36 / 43 章

plt.style:整套换外观

一个 style 就是一串 键: 值 组成的预设,plt.style.use() 把它批量灌进 rcParams。

Python7 行
import matplotlib.pyplot as plt

plt.style.use('seaborn-v0_8-whitegrid')                 # 全局切换(名字或文件路径)
plt.style.use(['dark_background', 'grayscale'])         # 列表:后面的覆盖前面的
plt.style.use({'axes.grid': True})                      # 也可以直接给字典
with plt.style.context('ggplot'):                       # 临时生效,出栈还原
    ...
用法 一句话 说明
use(name) 全局切换 会叠加在当前 rcParams 上,不是整套替换
use([a, b]) 多套叠加 后面的赢
use(dict) 直接改键 相当于批量 rcParams.update()
context(name) 临时 出栈还原,异常也还原
available 可用名字列表 列名字只用它
library {名字: RcParams} 想查某套改了哪些键才用它
reload_library() 重扫样式目录 放了自己的 .mplstyle 后调一次

最小示例

Python13 行
import matplotlib as mpl
import matplotlib.pyplot as plt

print(len(plt.style.available), len(plt.style.library))
# 输出: 29 29
print(sorted(set(plt.style.library) - set(plt.style.available)))
# 输出: []
print('default' in plt.style.library)
# 输出: False

plt.style.use('dark_background')
print(mpl.rcParams['axes.facecolor'], mpl.rcParams['axes.edgecolor'])
# 输出: black white

available 和 library 数量一致(29 vs 29),差集是空——旧资料写的“28 vs 31,library 里多三个下划线开头的内部样式”在 3.10.7 已经不成立:_classic_test_patch、_mpl-gallery 这些下划线样式也出现在 available 里了。

'default' 不在 library 里,但 use('default') 仍然合法——它是保留名:内部展开成整套出厂默认值(取 rcParamsDefault,跳过 backend 这类黑名单键),效果接近 mpl.rcdefaults() 但不动后端。

六种内置 style 并排

use 是叠加,不是替换

这一点决定了“换 style 之后为什么还有个键没变”:

Python11 行
import matplotlib as mpl
import matplotlib.pyplot as plt

mpl.rcParams['axes.grid'] = True
plt.style.use('dark_background')
print(mpl.rcParams['axes.grid'], mpl.rcParams['axes.facecolor'])
# 输出: True black         ← grid 是我之前设的,dark_background 没管它

plt.style.use('grayscale')
print(mpl.rcParams['axes.grid'], mpl.rcParams['axes.facecolor'])
# 输出: True white

dark_background 只有 17 个键,grayscale 16 个——它们只管自己列出的那些键,其余的从上一个状态继承。所以同一进程里连续 use 会累积。要干净的起点,先 plt.style.use('default') 或 mpl.rcdefaults()。

各套 style 的键数差别极大:

Python14 行
import matplotlib.pyplot as plt

for n in ['fast', 'tableau-colorblind10', 'grayscale', 'dark_background',
          'ggplot', 'seaborn-v0_8-whitegrid', 'fivethirtyeight', 'bmh', 'classic']:
    print(f'{n:24s} {len(plt.style.library[n]):3d}')
# 输出: fast                        3
# 输出: tableau-colorblind10        2
# 输出: grayscale                  16
# 输出: dark_background            17
# 输出: ggplot                     22
# 输出: seaborn-v0_8-whitegrid     25
# 输出: fivethirtyeight            26
# 输出: bmh                        19
# 输出: classic                   258

classic 有 258 个键——它是“整套还原到 1.x 外观”,几乎每个键都要重设;tableau-colorblind10 只有 2 个键,它只想换配色循环。

列表叠加:后面的覆盖前面的

Python10 行
import matplotlib as mpl
import matplotlib.pyplot as plt

with plt.style.context(['dark_background', 'grayscale']):
    print(mpl.rcParams['axes.facecolor'])
# 输出: white

with plt.style.context(['grayscale', 'dark_background']):
    print(mpl.rcParams['axes.facecolor'])
# 输出: black

顺序决定结果:[A, B] 先用 A 再用 B,冲突的键 B 赢。想让“深色底 + 我的自定义配色”,就写 ['dark_background', {'axes.prop_cycle': ...}]——字典放最后兜底。

reload_library():放了新 style 文件之后

available 是进程启动时扫盘扫出来的。把自建的 .mplstyle 丢进 <configdir>/stylelib/ 之后,不调 reload_library() 就永远看不到它:

Python10 行
import matplotlib as mpl
import matplotlib.pyplot as plt

print(mpl.get_configdir())
# 输出: C:\Users\wx\.matplotlib
print(len(plt.style.available))
# 输出: 29
plt.style.reload_library()                  # 重扫 stylelib/,不用重启进程
print(len(plt.style.available))
# 输出: 29     ← 目录里没有新文件时,数量不变

完整流程是三步:把 mine.mplstyle 放进 <configdir>/stylelib/ → plt.style.reload_library() → plt.style.use('mine')。

get_configdir() 在 matplotlib 上,pyplot 上没有这个函数——写 plt.get_configdir() 是 AttributeError。这也是“为什么不能只 import matplotlib.pyplot as plt“的例子之一。

注意事项

  • use() 是叠加,不是替换;连续 use 会累积,想干净起点先 use('default') 或 rcdefaults()。
  • 列名字用 available;library 是字典,print 出来几百行,只在查“某套改了哪些键”时取单个。
  • 'default' 不在 library 里,但 use('default') 合法(保留名,展开成整套出厂值,不动 backend)。
  • 自建 style 要放 get_configdir()/stylelib/ 并 reload_library()。
  • plt.style.context 能吃列表和字典;mpl.rc_context 只能吃字典。

导出与复用自己的样式

rcParams 改到满意之后,怎么变成可复用的 .mplstyle?不能用 savefig:

Python9 行
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
try:
    fig.savefig('X_one.mplstyle')
except ValueError as e:
    print('ValueError', str(e)[:64])
# 输出: ValueError Format 'mplstyle' is not supported (supported formats: eps, jpeg
plt.close(fig)

也没法直接把 rcParams 打印出来当文件用——repr() 出来的是 Python 字典,不是 ini:

Python4 行
import matplotlib as mpl
from pathlib import Path

Path('X_repr.mplstyle').write_text(repr(mpl.rcParams), encoding='utf-8')

这个文件共 367 行,挂上去会刷出 322 条 Bad key + 45 条 Missing colon(repr 里键带引号、长列表值跨行),367 行一个键都没被接受——len(rcParams) 仍是 322,等于什么都没读进去。它不是“报错停止”,而是“逐行跳过、静默失败”,最坑的一种。

正确做法是自己遍历,只写该写的键:

Python47 行
import enum
import matplotlib as mpl
import matplotlib.pyplot as plt
from pathlib import Path

# 与"样式"无关、写回去会破坏解析的键,必须排除
SKIP = {'backend', 'backend_fallback', 'timezone', 'interactive', 'toolbar',
        'figure.hooks', 'figure.max_open_warning', 'webagg.address', 'webagg.port',
        'webagg.port_retries', 'webagg.open_in_browser', 'tk.window_focus',
        'figure.raise_window', 'date.epoch', 'docstring.hardcopy', 'savefig.directory'}

def _fmt(v):
    if isinstance(v, enum.Enum):                    # CapStyle.butt → butt
        return v.name
    if isinstance(v, str):
        return v[1:] if v.startswith('#') else v     # '#' 是 ini 的注释符
    return str(v)

def dump_style(path, **overrides):
    """把当前 rcParams 里的样式类键导出成 .mplstyle(ini 格式)。"""
    mpl.rcParams.update(overrides)
    lines = []
    for k, v in mpl.rcParams.items():
        if k.startswith('_') or k in SKIP or isinstance(v, dict):
            continue
        if v is None or (isinstance(v, (list, tuple, str)) and len(v) == 0):
            continue                                  # 空值写回去会变成字符串
        if k == 'axes.prop_cycle':                    # Cycler:既不是字符串也不是列表
            cols = [str(c).lstrip('#') for c in v.by_key()['color']]
            lines.append("axes.prop_cycle: cycler('color', [%s])"
                         % ", ".join(f"'{c}'" for c in cols))
        elif isinstance(v, (list, tuple)):
            lines.append(f'{k}: {", ".join(_fmt(x) for x in v)}')
        else:
            lines.append(f'{k}: {_fmt(v)}')
    Path(path).write_text('\n'.join(lines) + '\n', encoding='utf-8')
    return len(lines)

n = dump_style('X_house.mplstyle',
               **{'axes.grid': False, 'axes.spines.top': False, 'font.size': 12,
                  'figure.figsize': [8, 5], 'axes.unicode_minus': False})

mpl.rcdefaults()
plt.style.use('X_house.mplstyle')            # 写出的文件必须能零告警读回
print(n, mpl.rcParams['font.size'], mpl.rcParams['axes.grid'],
      mpl.rcParams['figure.figsize'], mpl.rcParams['grid.color'])
# 输出: 291 12.0 False [8.0, 5.0] #b0b0b0

四条序列化规则,缺一条就会读出告警或静默失效:

  • 列表值不加方括号,用逗号拼平(k: v1, v2)。加了方括号会被当成普通字符吃进去。
  • 字符串里的 # 要去掉。ini 里 # 是注释起始符,grid.color: #b0b0b0 会被截成空值;写 b0b0b0 就行,读回时 validate_color 会自动补上 #——所以上面打印出来的是 '#b0b0b0'。
  • 枚举取 .name。lines.dash_capstyle 是 CapStyle.butt 枚举,str() 得到 'CapStyle.butt',读回时报 not a valid CapStyle;要写 butt。
  • Cycler 单独处理:它既不是字符串也不是列表,得拼成 cycler('color', ['1f77b4', ...]) 这种构造表达式。

还有一条:跳过 None 和空容器。path.effects 默认是 [],写成 path.effects: [] 会被解析成含一个空字符串的列表,然后每次画图都炸(下面有实测)。

这四条都是实测撞出来的,第一版 dump_style 少了后三条,导出的文件读回时刷出 6 条 Bad value:axes.prop_cycle 被 # 截断成“unterminated string literal”、grid.color 变空串、四个 capstyle/joinstyle 全不认。写完必须 use 一次并检查 stderr,这是唯一可靠的验收方式。

三个“写进去就废”的键

figure.hooks 和 path.effects 是实测踩过的:

Python26 行
import matplotlib as mpl
import matplotlib.pyplot as plt
from pathlib import Path

Path('X_hook.mplstyle').write_text("figure.hooks: ['a:b']\n", encoding='utf-8')
plt.style.use('X_hook.mplstyle')
print(mpl.rcParams['figure.hooks'])
# 输出: ["['a:b']"]          ← 整个列表被当成一个字符串
try:
    plt.figure()
except ModuleNotFoundError as e:
    print('ModuleNotFoundError', e)
# 输出: ModuleNotFoundError No module named "['a"

mpl.rcdefaults()
Path('X_eff.mplstyle').write_text('path.effects: []\n', encoding='utf-8')
plt.style.use('X_eff.mplstyle')
print(mpl.rcParams['path.effects'])
# 输出: ['[]']               ← 空列表变成一个空串元素
try:
    fig, ax = plt.subplots()
    ax.plot([1, 2])
    fig.savefig('X_eff.png')
except AttributeError as e:
    print('AttributeError', e)
# 输出: AttributeError 'str' object has no attribute 'draw_path'

figure.hooks 的值必须是 ['模块:函数'] 这种真列表;style.use 解析时只会按逗号切普通字符串,于是 ["['a:b']"] 被整块当成一个模块名去 import,报 No module named "['a"。 path.effects 的类型是“PathEffect 或 None 的列表”,写 [] 得到 ['[]'](一个字符串 '[]'),画线时拿它当 PathEffect 用就 AttributeError。

结论:这两个键(以及 SKIP 里那些)不要导出。要改它们就老老实实写 Python。

注意事项

  • savefig('*.mplstyle') 直接 ValueError: Format 'mplstyle' is not supported。
  • 别把 repr(rcParams) 当 style 文件:322 行 Bad key + 45 行 Missing colon,且静默失效。
  • 列表值不加方括号;None 和空容器全部跳过。
  • figure.hooks / path.effects 不能靠 .mplstyle 设。
  • 只写“与该主题不同”的键更省事——几十行、能看、不会带进无关配置。
  • 导出后必须 use 一次并新开一张图验证;“文件写出了但键没生效”是这里的常态。

.mplstyle 语法:手写一份

手写比导出干净得多。格式就是 matplotlibrc 的 ini 语法:键: 值,注释用 # 或 ;。

Python26 行
import matplotlib as mpl
import matplotlib.pyplot as plt
from pathlib import Path

Path('X_mystyle.mplstyle').write_text("""\
# 手写主题:白底 + 只留左侧轴线 + 仅 y 向网格
axes.grid: True
axes.grid.axis: y
grid.color: e3e3e3
axes.spines.top: False
axes.spines.right: False
axes.titlesize: large
axes.titleweight: bold
lines.linewidth: 1.2
legend.frameon: False
figure.figsize: 8, 5
font.size: 11
font.sans-serif: Microsoft YaHei, DejaVu Sans
axes.unicode_minus: False
""", encoding='utf-8')

plt.style.use('X_mystyle.mplstyle')
print(mpl.rcParams['figure.figsize'], mpl.rcParams['axes.grid'], mpl.rcParams['font.size'])
# 输出: [8.0, 5.0] True 11.0
print(mpl.rcParams['font.sans-serif'][:2], mpl.rcParams['grid.color'])
# 输出: ['Microsoft YaHei', 'DejaVu Sans'] #e3e3e3

注意 grid.color 写的是 e3e3e3 而不是 #e3e3e3——见下一小节。

# 是注释起始符:颜色写成 e3e3e3

这是 ini 格式和 Python 语法冲突最明显的一处。颜色值以 # 开头,而 # 在 ini 里开始一条注释,于是值被截成空串:

Python15 行
import matplotlib as mpl
import matplotlib.pyplot as plt
from pathlib import Path

mpl.rcdefaults()
Path('X_ok_color.mplstyle').write_text('grid.color: e3e3e3\n', encoding='utf-8')
plt.style.use('X_ok_color.mplstyle')
print(mpl.rcParams['grid.color'])
# 输出: #e3e3e3          ← 自动补上了 #

mpl.rcdefaults()
Path('X_bad_color.mplstyle').write_text('grid.color: #e3e3e3\n', encoding='utf-8')
plt.style.use('X_bad_color.mplstyle')
print(mpl.rcParams['grid.color'])
# 输出: #b0b0b0          ← 没生效,还是默认值(stderr 里一条 Bad value)

关键在于 rc 的颜色校验器会帮你补 #:validate_color 收到 6 位字符串且不以 # 开头时,自动在前面加一个。所以 rc 文件里必须写 e3e3e3,而这不代表随便什么地方都能省 #:

Python9 行
import matplotlib as mpl

try:
    mpl.colors.to_rgba('e3e3e3')
except ValueError as e:
    print('ValueError', e)
# 输出: ValueError Invalid RGBA argument: 'e3e3e3'
print(mpl.colors.to_rgba('#e3e3e3')[:3])
# 输出: (0.8901960784313725, 0.8901960784313725, 0.8901960784313725)

同一个 'e3e3e3',在 .mplstyle 里合法、在 to_rgba 里立刻报错——差别只在“这一层有没有 validate_color 兜底”。同理 plot(color='e3e3e3') 也会报错。

列表值:不要加方括号

这是本机实测最容易错的一处。matplotlib 的 ini 解析器对列表值只按逗号切,方括号不会被理解成“列表语法”,而是当成普通字符留在值里:

Python13 行
import matplotlib as mpl
import matplotlib.pyplot as plt
from pathlib import Path

Path('X_bad.mplstyle').write_text("font.sans-serif: ['AAA', 'BBB']\n", encoding='utf-8')
plt.style.use('X_bad.mplstyle')
print(mpl.rcParams['font.sans-serif'])
# 输出: ["['AAA'", "'BBB']"]     ← 方括号和单引号都成了字符

Path('X_ok.mplstyle').write_text('font.sans-serif: AAA, BBB\n', encoding='utf-8')
plt.style.use('X_ok.mplstyle')
print(mpl.rcParams['font.sans-serif'])
# 输出: ['AAA', 'BBB']

所以 font.sans-serif: ['Microsoft YaHei', 'DejaVu Sans'] 是错的,正确写法是 font.sans-serif: Microsoft YaHei, DejaVu Sans——不加方括号、不加引号。这一条和“Python 里写列表”的直觉正好相反。

figure.figsize 用逗号分隔

Python8 行
import matplotlib as mpl
import matplotlib.pyplot as plt
from pathlib import Path

Path('X_fig.mplstyle').write_text('figure.figsize: 8, 5\n', encoding='utf-8')
plt.style.use('X_fig.mplstyle')
print(mpl.rcParams['figure.figsize'])
# 输出: [8.0, 5.0]

8 x 5、(8, 5)、[8, 5] 都不行——就是这个逗号形式。

axes.prop_cycle 要写 cycler(...)

配色循环不是一个颜色列表,而是一个 Cycler 对象,所以文件里得写它的构造表达式:

Python10 行
import matplotlib as mpl
import matplotlib.pyplot as plt
from pathlib import Path

Path('X_cyc.mplstyle').write_text(
    "axes.prop_cycle: cycler('color', ['0072B2', 'D55E00', '009E73'])\n",
    encoding='utf-8')
plt.style.use('X_cyc.mplstyle')
print(mpl.rcParams['axes.prop_cycle'].by_key()['color'])
# 输出: ['#0072B2', '#D55E00', '#009E73']     ← 不带 # 也能认

颜色同样不带 #(原因见上一小节)。解析器把这个字符串当表达式求值成 Cycler;而在 Python 里设同一个键必须给 cycler 对象,给裸列表会报错:

Python12 行
import matplotlib as mpl
from cycler import cycler

try:
    mpl.rcParams['axes.prop_cycle'] = ['#0072B2', '#D55E00']
except ValueError as e:
    print('ValueError', str(e)[:60])
# 输出: ValueError Key axes.prop_cycle: Object is not a string or Cycler instan

mpl.rcParams['axes.prop_cycle'] = cycler('color', ['#0072B2', '#D55E00'])
print(mpl.rcParams['axes.prop_cycle'].by_key()['color'])
# 输出: ['#0072B2', '#D55E00']

路径:名字、文件、绝对路径都行

Python15 行
import matplotlib as mpl
import matplotlib.pyplot as plt
from pathlib import Path

try:
    plt.style.use('X_no_such_style')
except OSError as e:
    print('OSError', str(e)[:72])
# 输出: OSError 'X_no_such_style' is not a valid package style, path of style file, UR

Path('X_mystyle.mplstyle').write_text('font.size: 13\n', encoding='utf-8')
plt.style.use(Path('X_mystyle.mplstyle').resolve())      # 绝对路径,最稳
print(mpl.rcParams['font.size'])
# 输出: 13.0
plt.style.use('default')

use() 的查找顺序是:available 里的名字 → 当前目录下的文件 → 给了路径就按路径。报 OSError: not a valid package style, path of style file, URL of style file, or library style name 时,九成是文件真的不在(或不在 cwd),不是格式错。传 Path(...).resolve() 能省掉这类猜谜。

注意事项

  • 注释用 # 或 ;;值里不能出现 #(颜色写 0072B2)。
  • 列表值不加方括号、不加引号,逗号分隔。
  • figure.figsize 写 8, 5。
  • axes.prop_cycle 写 cycler('color', [...])。
  • 键名写错不会报错,只是不生效;值类型错才报错。写完一定要 use 一次再读回来看。
  • 相对路径依赖 cwd;脚本里建议绝对路径。

颜色循环与主题一致性

同一个“东北/华东/华南”在五张图里必须是同一个颜色。靠默认配色循环做不到——它按第几条线分配,加一条线就整体错位。要做主题一致性,得把“业务 → 颜色”这个映射写死,循环序列只是兜底。

默认 10 色 vs 自定义 3 色循环

axes.prop_cycle:循环序列

Python5 行
import matplotlib as mpl

print(mpl.rcParams['axes.prop_cycle'].by_key()['color'])
# 输出: ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2',
# 输出:  '#7f7f7f', '#bcbd22', '#17becf']

默认 10 色。改成自己的 3 色后,第 4、5 条线会回到第 1、2 色——不是自动降亮度:

Python10 行
import matplotlib.pyplot as plt
from cycler import cycler

plt.rcParams['axes.prop_cycle'] = cycler(color=['#0072B2', '#D55E00', '#009E73'])
fig, ax = plt.subplots()
for i in range(5):
    ax.plot([i, i + 1], label=f'L{i}')
print([ln.get_color() for ln in ax.get_lines()])
# 输出: ['#0072B2', '#D55E00', '#009E73', '#0072B2', '#D55E00']
plt.close(fig)

颜色数量少于系列数量时,循环复用是默认行为,不会警告。这意味着两条不同的线可能同色——如果语义上要求可区分,就得自己保证颜色够用,或者干脆显式指定。

cycler 还能一次绑多个属性(颜色 + 线型 + 标记),这是“按第几条线换外观”的正确工具:

Python12 行
import matplotlib as mpl
import matplotlib.pyplot as plt
from cycler import cycler

cyc = cycler(color=['#0072B2', '#D55E00']) * cycler(linestyle=['-', '--'])
mpl.rcParams['axes.prop_cycle'] = cyc
fig, ax = plt.subplots()
for i in range(4):
    ax.plot([0, 1], [i, i + 1])
print([(ln.get_color(), ln.get_linestyle()) for ln in ax.get_lines()])
# 输出: [('#0072B2', '-'), ('#0072B2', '--'), ('#D55E00', '-'), ('#D55E00', '--')]
plt.close(fig)

* 是笛卡尔积:2 色 × 2 线型 = 4 种组合,但顺序是内层(后一个 cycler)变得快——先在同一条颜色上换完所有线型,再换下一个颜色。写成 + 则是串接。

显式映射:业务色优先

真正要“跨图一致”时,别指望循环——把映射写成一个字典,每次 plot 显式传 color:

Python15 行
import matplotlib as mpl
import matplotlib.pyplot as plt

BRAND = {'东北': '#0072B2', '华东': '#D55E00', '华南': '#009E73', '西南': '#CC79A7'}
mpl.rcParams['axes.prop_cycle'] = mpl.cycler(color=list(BRAND.values()))
print(mpl.rcParams['axes.prop_cycle'].by_key()['color'])
# 输出: ['#0072B2', '#D55E00', '#009E73', '#CC79A7']

fig, axs = plt.subplots(1, 2, figsize=(7, 2.4))
for ax in axs:
    for i, (name, color) in enumerate(BRAND.items()):
        ax.plot([0, 1], [i, i + 1], color=color, label=name)
axs[0].legend(fontsize=7, ncol=2)
axs[1].legend(fontsize=7, ncol=2)
plt.close(fig)

两张图颜色完全一致,因为 color=BRAND[name] 覆盖了循环——prop_cycle 只是“没给 color 时”的兜底。把 prop_cycle 也设成同一组颜色,则是让“没显式指定的图”自动对齐主题。

调色板本身怎么选是另一件事:分类色要过色盲可辨(tableau-colorblind10、petroff10 都在 style.available 里),连续量用 viridis 这类感知均匀的 cmap。别用 jet。

注意事项

  • prop_cycle 按“第几条线”分配,加一条线就整体错位;跨图一致必须显式 color=。
  • 颜色不够用时循环复用,不报警——可能出现两条语义不同的线同色。
  • 赋值必须是 cycler 对象,裸列表报 ValueError: Object is not a string or Cycler instance。
  • cycler(a) * cycler(b) 是笛卡尔积,+ 是串接,别混。
  • style.available 里带 colorblind 字样的几套是现成的色盲友好配色,做分类色时先试它们。