目录49

文章Matplotlib第 27 章

颜色、色图与 colorbar

Matplotlib · 第 27 / 43 章

颜色表示方式

matplotlib 里「一个颜色」有九种合法写法,全部由 matplotlib.colors.to_rgba() 统一解析成 (r, g, b, a) 四元组。记住这一条就能解释所有「颜色参数不生效」的问题:任何接颜色的地方,最终都过这个函数。

Python4 行
matplotlib.colors.to_rgba(c, alpha=None)     # 把任意写法解析成 (r, g, b, a),各分量 0~1
matplotlib.colors.to_hex(c, keep_alpha=False)  # 解析成 '#rrggbb' 字符串
matplotlib.colors.to_rgb(c)                  # 只要 RGB 三元组
matplotlib.colors.is_color_like(c)           # 判断是不是合法颜色(不抛异常)
写法 例子 说明
十六进制 6 位 '#1f77b4' 最常用,#RRGGBB
十六进制 3 位 '#C0C' #C0C = #CC00CC,只有每位都重复时才能缩写
十六进制 8 位 '#1f77b480' 后两位是 alpha
CSS4 名字 'crimson' 148 个,见 CSS4_COLORS
单字母码 'b' 'r' 'k' 只有 8 个,见 BASE_COLORS
Tableau 名 'tab:blue' 10 个,见 TABLEAU_COLORS
循环引用 'C0'~'C9' 跟随 axes.prop_cycle 的第 N 色
灰度字符串 '0.5' 必须是字符串;0=黑 1=白
RGB(A) 元组 (1, .5, 0) / (1, .5, 0, .3) 分量 0~1 浮点
无色 'none' 完全透明;区别见下方注意事项

最小示例

Python11 行
import matplotlib as mpl

for c in ['#1f77b4', 'tab:blue', 'C0', 'crimson', '0.5', (1, .5, 0), (1, .5, 0, .3)]:
    print(f'{str(c):18}', mpl.colors.to_rgba(c))
# 输出: #1f77b4            (0.12156862745098039, 0.4666666666666667, 0.7058823529411765, 1.0)
# 输出: tab:blue           (0.12156862745098039, 0.4666666666666667, 0.7058823529411765, 1.0)
# 输出: C0                 (0.12156862745098039, 0.4666666666666667, 0.7058823529411765, 1.0)
# 输出: crimson            (0.8627450980392157, 0.0784313725490196, 0.23529411764705882, 1.0)
# 输出: 0.5                (0.5, 0.5, 0.5, 1.0)
# 输出: (1, 0.5, 0)        (1.0, 0.5, 0.0, 1)
# 输出: (1, 0.5, 0, 0.3)   (1.0, 0.5, 0.0, 0.3)

九种颜色写法的实际色块

三张色表:CSS4 / BASE / TABLEAU

Python10 行
import matplotlib as mpl

print(len(mpl.colors.CSS4_COLORS), len(mpl.colors.BASE_COLORS), len(mpl.colors.TABLEAU_COLORS))
# 输出: 148 8 10

print(list(mpl.colors.BASE_COLORS))
# 输出: ['b', 'g', 'r', 'c', 'm', 'y', 'k', 'w']

print(mpl.colors.CSS4_COLORS['crimson'])
# 输出: #DC143C

CSS4_COLORS 的值是十六进制字符串,不是元组;TABLEAU_COLORS 的键里带冒号('tab:blue'),所以写 'tab.blue' 会报错。

灰度必须写成字符串

Python7 行
import matplotlib as mpl

print(mpl.colors.to_rgba('0.5'))
# 输出: (0.5, 0.5, 0.5, 1.0)

print(mpl.colors.to_rgba(0.5))
# ValueError: Invalid RGBA argument: 0.5

浮点 0.5 会被当成「单元素的 RGBA 序列」去解析,长度不对直接 ValueError。灰度一律加引号。

三位十六进制:'#C0' 不合法

Python9 行
import matplotlib as mpl

for bad in ['#C0', '#c0']:
    try:
        mpl.colors.to_rgba(bad)
    except ValueError as e:
        print(e)
# 输出: Invalid RGBA argument: '#C0'
# 输出: Invalid RGBA argument: '#c0'

报的是 Invalid RGBA argument 而不是「hex 格式错误」——因为解析器把所有写法都当「RGBA 参数」试一遍,全失败才报这个。合法形式只有 3 / 6 / 8 位。

'C0' 能被解析,但不在命名色表里

Python6 行
import matplotlib as mpl

print(mpl.colors.to_hex('C0'))
# 输出: #1f77b4
print(mpl.colors.is_color_like('C0'), 'C0' in mpl.colors.get_named_colors_mapping())
# 输出: True False

to_rgba('C0') 能出值,是因为解析时会去查当前的 prop_cycle;但它不在命名色表里。所以 cycler(color=['C0']) 会被拒(见下一节)。

注意事项

  • 灰度必须写 '0.5',写 0.5 抛 ValueError: Invalid RGBA argument: 0.5。
  • 十六进制只有 3 / 6 / 8 位合法,'#C0'、'#c0' 都是 Invalid RGBA argument。
  • 三位缩写要每位都成立:'#C0C' = '#CC00CC',不是「补零」。
  • RGB 元组分量是 0~1 浮点,写 (255, 128, 0) 会因超出范围报错或得到意外结果。
  • 'none' 与 'None' 等价,表示完全不画;patch 里 fc='none', ec='k' 是「无填充有边」。
  • 'C0' 依赖当前 prop_cycle,改了 cycle 颜色就变;要固定颜色写十六进制或 'tab:x'。
  • is_color_like('okabe_ito') 是 False——命名色板名不是颜色,不能直接当颜色用。

prop_cycle 主题色循环

prop_cycle 是「不指定颜色时,第 n 条线该用什么颜色」的那张表。它存在 rcParams['axes.prop_cycle'] 里,默认 10 色。

Python9 行
rcParams["axes.prop_cycle"] = cycler(
    color=["#1f77b4", ...]      # 颜色列表,必须是真实颜色(不能是 'C0')
)
Axes.set_prop_cycle(
    color=[...],                # 只设颜色
    linestyle=["--", ":"],      # 同时轮换线型
    linewidth=[1, 2, 3],        # 还能轮换线宽
    **kwargs                    # 任意 Artist 属性都能进 cycle
)
写法 一句话 默认值的影响
cycler(color=[...]) 只轮换颜色 默认 10 色
cycler(color=..., linewidth=...) 两个属性同时逐项配对 只用 color 时线宽恒定
cycler(...) + cycler(...) 逐项配对,长度须相等 长度不等报 ValueError
cycler(...) * cycler(...) 笛卡尔积 长度相乘
cycler(...)[:3] 切片取前 3 项 —
set_prop_cycle 只作用于当前 Axes rcParams 是全局

最小示例

Python11 行
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
from cycler import cycler

mpl.rcParams['axes.prop_cycle'] = cycler(color=['tab:blue', 'tab:red', 'k'])

fig, ax = plt.subplots()
for i in range(4):
    ax.plot(np.arange(10), np.arange(10) + i)
plt.show()

默认 10 色与全局改法

Python7 行
import matplotlib as mpl

cols = mpl.rcParams['axes.prop_cycle'].by_key()['color']
print(len(cols))
# 输出: 10
print(cols[0])
# 输出: #1f77b4

by_key()['color'] 拿到的已经是十六进制字符串(不是元组),可以直接 to_hex 之外的场合复用。

全局改 rcParams 影响之后新建的所有图;ax.set_prop_cycle() 只影响这个 axes 之后画的 artist。

'C0' 不能进 cycle:这是循环引用

Python8 行
import matplotlib as mpl
from cycler import cycler

try:
    mpl.rcParams['axes.prop_cycle'] = cycler(color=['C0', 'C1'])
except ValueError as e:
    print(e)
# 输出: Key axes.prop_cycle: Cannot put cycle reference ('C0') in prop_cycler

'C0' 的含义是「cycle 里的第 0 个颜色」——把它写进 cycle 本身就成了自指。要固定色值就用 'tab:blue' / 十六进制;plot(color='C0') 这种单点使用是合法的。

命名色板(okabe_ito 等)也不能直接进 cycle

Python8 行
import matplotlib as mpl
from cycler import cycler

try:
    mpl.rcParams['axes.prop_cycle'] = cycler(color=['okabe_ito'])
except ValueError as e:
    print(e)
# 输出: Key axes.prop_cycle: 'okabe_ito' does not look like a color arg

命名色板是 colormap,不是颜色。要把它变成 cycle,得先取出具体色值:

Python7 行
import matplotlib as mpl
from cycler import cycler

OI = [mpl.colors.to_hex(mpl.colormaps['tab10'](i / 9)) for i in range(10)]
print(OI[:3])
# 输出: ['#1f77b4', '#ff7f0e', '#2ca02c']
mpl.rcParams['axes.prop_cycle'] = cycler(color=OI)

上面用 tab10 演示,是因为 okabe_ito 在 3.10 上不存在(见下方版本差异)。

同时轮换颜色与线型

Python14 行
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
from cycler import cycler

mpl.rcParams['axes.prop_cycle'] = (cycler(color=['r', 'g', 'b'])
                                   + cycler(linestyle=['-', '--', ':']))
print(len(mpl.rcParams['axes.prop_cycle']))
# 输出: 3

fig, ax = plt.subplots()
for i in range(3):
    ax.plot(np.arange(5), np.arange(5) + i)
plt.show()

+ 是逐项配对(红配实线、绿配虚线……),要求两边长度相等;长度不等直接报错:

Python7 行
from cycler import cycler

try:
    cycler(color=['r', 'g']) + cycler(linewidth=[1, 2, 3])
except ValueError as e:
    print(e)
# 输出: Can only add equal length cycles, not 2 and 3

* 是笛卡尔积,3 色 × 3 线型 = 9 种组合:

Python4 行
from cycler import cycler

print(len(cycler(color=['r', 'g', 'b']) * cycler(linestyle=['-', '--', ':'])))
# 输出: 9

所以「颜色和线型都要轮换、但只想画 3 条线」用 +;「穷举所有组合」用 *。

set_prop_cycle 只管之后的 artist

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

fig, ax = plt.subplots()
ax.plot(np.arange(5), label='改之前')
ax.set_prop_cycle(color=['r', 'g'], linestyle=['--', ':'])
ax.plot(np.arange(5), np.arange(5) + 1, label='改之后')
ax.legend()
plt.show()

第一条线保持默认蓝,第二条才开始走新 cycle。想让已有 artist 变色只能逐条 set_color。

版本差异:3.10 上什么没有

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

print('okabe_ito' in plt.colormaps(), 'petroff6' in plt.colormaps())
# 输出: False False
print(hasattr(mpl.colormaps['Blues'], 'with_alpha'))
# 输出: False

okabe_ito / petroff6 / petroff8 和 Colormap.with_alpha() 都是 3.11 新增,3.10 上不存在。看到教程里直接 cycler(color=['okabe_ito']) 或 cmap.with_alpha(.5),先 hasattr 确认版本。

注意事项

  • cycler(color=['C0']) 报 Cannot put cycle reference ('C0') in prop_cycler;cycle 里只能放真实颜色。
  • 命名色板名('okabe_ito'、'tab10')不是颜色,进 cycle 报 does not look like a color arg。
  • set_prop_cycle 对已存在的 artist 无效,只影响之后新建的。
  • rcParams 是全局状态,改完记得恢复,否则后面的图全变色。
  • cycler(a) + cycler(b) 是逐项配对(长度必须相等),cycler(a) * cycler(b) 是笛卡尔积(长度相乘)——两者反了会导致图例颜色对不上线型。
  • 3.10 没有 okabe_ito 和 with_alpha,写兼容代码要先 hasattr。

colormap 分类

colormap(色图)把 [0, 1] 的标量映射成颜色。选错色图比配色难看更严重——非感知均匀的色图会伪造出数据里不存在的高低边界。

用途 推荐 避开
连续数值(大小/强度) viridis plasma magma cividis Blues jet rainbow(非感知均匀,有假边界)
有中点意义的双向 coolwarm RdBu BrBG RdYlBu 自制对称渐变
离散类别 ≤10 tab10 Set2 Paired jet
二维相关 berlin managua vanimo 强行转一维
Python6 行
import matplotlib.pyplot as plt

print(len(plt.colormaps()))
# 输出: 180
print(sum(1 for n in plt.colormaps() if n.endswith('_r')))
# 输出: 90

colormap 三类:定性 / 顺序 / 发散

三类的判据

  • 定性(qualitative):类别之间没有大小关系,色图应该「越不连续越好」。tab10 是最常用的 10 色。
  • 顺序(sequential):单变量从低到高,色图应该「亮度单调」,这样黑白打印也能读。
  • 发散(diverging):以某个中点(通常是 0)向两边发散,中点应该是浅色(白/灰)。

_r 后缀 = 反向

Python8 行
import matplotlib as mpl

print(mpl.colors.to_hex(mpl.colormaps['Blues'](0.0)),
      mpl.colors.to_hex(mpl.colormaps['Blues'](1.0)))
# 输出: #f7fbff #08306b

print(mpl.colors.to_hex(mpl.colormaps['Blues_r'](0.0)))
# 输出: #08306b          ← 和 Blues(1.0) 是同一个色

每个 colormap 都自动带一个 _r 反向版,所以 180 个里有 90 组。

采样:把一个 colormap 当调色板用

Python8 行
import matplotlib as mpl
import numpy as np

cm = mpl.colormaps['viridis']
print([mpl.colors.to_hex(cm(t)) for t in (0, .5, 1)])
# 输出: ['#440154', '#21918c', '#fde725']
print(cm(np.array([0, 0.5, 1.0])).shape)
# 输出: (3, 4)

传数组会一次拿到 shape (N, 4) 的 RGBA,比循环快。cm(t) 里的 t 必须是 [0, 1];给数据原值要先用 Normalize 归一化。

离散化:resampled()

Python4 行
import matplotlib as mpl

print(mpl.colormaps['Blues'].resampled(6).N)
# 输出: 6

resampled(N) 把连续色图压成 N 级,配合 BoundaryNorm 做分段填色。

注意事项

  • jet / rainbow 不是感知均匀的,会在中间伪造出「亮带」,正式图表别用。
  • 顺序色图要选亮度单调的(viridis Blues Greys),否则灰度打印读不出高低。
  • 发散色图的中段必须是浅色,否则「0」看起来像有强度。
  • plt.colormaps() 在 3.10 是 180 个(含 90 个 _r);不同版本数量会变,别把数字写死进断言。
  • cm(t) 的 t 必须在 [0, 1],直接传原始数据会全挤到两端。
  • mpl.cm.get_cmap('viridis') 已在 3.9 移除,用 mpl.colormaps['viridis']。

Normalize:数据怎么映射成颜色

颜色映射是两步:Normalize 把数据压到 [0, 1],再交给 colormap 换成颜色。所以「同样的数据画出来颜色分布不对」几乎都是 Normalize 选错了,而不是 colormap 选错。

Python7 行
matplotlib.colors.Normalize(vmin=None, vmax=None, clip=False)
matplotlib.colors.LogNorm(vmin=None, vmax=None, clip=False)
matplotlib.colors.SymLogNorm(linthresh, linscale=1, vmin=None, vmax=None, clip=False)
matplotlib.colors.PowerNorm(gamma, vmin=None, vmax=None, clip=False)
matplotlib.colors.TwoSlopeNorm(vcenter=0, vmin=None, vmax=None)
matplotlib.colors.CenteredNorm(vcenter=0, halfrange=None, clip=False)
matplotlib.colors.BoundaryNorm(boundaries, ncolors, clip=False, extend="neither")
类 干什么 什么时候用
Normalize 线性映射 默认,数据分布均匀
LogNorm 对数映射 跨几个数量级,且全为正
SymLogNorm 0 附近线性、远处对数 有正有负且跨数量级
PowerNorm x**gamma 想人为拉开一头
TwoSlopeNorm 以某值分两段各自线性 有明确中点(如 0)且两侧范围不对称
CenteredNorm 以 0 为中心对称 数据有正有负,想 0 恒定对应中间色
BoundaryNorm 分段离散 配合 resampled() 做分级填色

最小示例

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

fig, ax = plt.subplots()
sc = ax.scatter(np.arange(10), np.arange(10) ** 2, c=np.arange(10) ** 2, s=60)
sc.set_norm(mpl.colors.Normalize(vmin=0, vmax=100))
cb = fig.colorbar(sc, ax=ax, label='平方值')
plt.show()

七种 Normalize 对同一个渐变的映射结果

Normalize 默认不定范围

Python5 行
import matplotlib as mpl

n = mpl.colors.Normalize()
print(n.vmin, n.vmax)
# 输出: None None

vmin/vmax 都是 None 时,会在第一次绘制时按数据自动定范围。这就是「改数据范围后颜色跟着变」的原因——要固定就显式给两个值。

clip=True 把超范围的值夹到两端(默认 False 时超范围值在 imshow 里仍会被夹,但 cmap() 直接调用会得到端点色的重复):

Python6 行
import matplotlib as mpl
import numpy as np

n = mpl.colors.Normalize(vmin=-1, vmax=1, clip=True)
print(n(np.array([-5., 0., 5.])))
# 输出: [0.  0.5 1. ]

LogNorm:负值不报错,直接变透明

Python5 行
import matplotlib as mpl
import numpy as np

print(mpl.colors.LogNorm(vmin=.01, vmax=100)(np.array([-1., 1., 10.])))
# 输出: [-- 0.0 1.0]

-- 是 MaskedArray 的掩码标记——负值被屏蔽,画出来是空白(不是黑色、也不是报错)。看到图上出现一块白底空洞,先怀疑 LogNorm 撞上了 0 或负值。

SymLogNorm:正负都有时的正解

Python6 行
import matplotlib as mpl
import numpy as np

n = mpl.colors.SymLogNorm(linthresh=1, vmin=-3, vmax=3)
print(np.round(n(np.array([-3., -1., 0., 1., 3.])), 3))
# 输出: [0.0 0.15 0.5 0.85 1.0]

linthresh 是「线性区」的宽度:|x| < linthresh 走线性,超出的走对数。设太小会让 0 附近的点挤成一团,设太大就退化成普通 Normalize。

PowerNorm:gamma 小于 1 时负值会出负数

Python6 行
import matplotlib as mpl
import numpy as np

n = mpl.colors.PowerNorm(gamma=.5, vmin=0, vmax=4)
print(n(np.array([-2., -1., 0., 2., 4.])))
# 输出: [-0.5  -0.25   0.    0.5   0.707]

注意 -2 变成 -0.5——PowerNorm 不裁剪负值,负数开方后仍是负的,映射结果落在 [0, 1] 之外。PowerNorm 只该用在非负数据上。

TwoSlopeNorm:中心值必须在范围内

Python7 行
import matplotlib as mpl

try:
    mpl.colors.TwoSlopeNorm(vcenter=5, vmin=-2, vmax=2)
except ValueError as e:
    print(e)
# 输出: vmin, vcenter, and vmax must be in ascending order

报错信息说的是「必须升序」,实际含义是 vmin < vcenter < vmax。用它之前先确认 vcenter 夹在数据范围里。

它的价值在于允许两侧范围不对称:vmin=-1, vcenter=0, vmax=9 时,0 依然对应色图中点。

CenteredNorm:以 0 为中心,自动取对称范围

Python6 行
import matplotlib as mpl
import numpy as np

n = mpl.colors.CenteredNorm()
print(np.round(n(np.array([-2., 0., 2.])), 3))
# 输出: [0.  0.5 1. ]

不给 vmin/vmax,自动取 max(|x|) 作为半幅,保证 0 恒定落在 0.5。数据里正负不对称时比 TwoSlopeNorm 省事。

BoundaryNorm:返回的是整数索引,不是 0~1

Python6 行
import matplotlib as mpl
import numpy as np

n = mpl.colors.BoundaryNorm([-2, -1, 0, 1, 2], ncolors=4)
print(n(np.array([-2., -1., 0., 1., 2.])))
# 输出: [0 1 2 3 4]

输出的 0..4 是颜色索引,不是归一化值。所以它必须配一个已经离散化的 colormap:

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

bounds = [-2.5, -1.5, -0.5, 0.5, 1.5, 2.5]
cmap = mpl.colormaps['coolwarm'].resampled(len(bounds) - 1)
norm = mpl.colors.BoundaryNorm(bounds, cmap.N)

fig, ax = plt.subplots()
im = ax.imshow(np.linspace(-3, 3, 144).reshape(12, 12), cmap=cmap, norm=norm)
fig.colorbar(im, ax=ax, ticks=bounds, boundaries=bounds, spacing='proportional')
plt.show()

如果 cmap 还是连续的(N=256),索引 0..4 会被当成 0..4 落在 [0,1] 之外,整张图变成同一种颜色——这是「用了 BoundaryNorm 图全糊成一片」的直接原因。

注意事项

  • Normalize() 默认 vmin=vmax=None,范围在首次绘制时按数据定;要固定必须显式给。
  • LogNorm 遇到 ≤0 的值不报错,而是掩码成透明;图上白块先怀疑它。
  • PowerNorm(gamma<1) 对负值输出负数,映射结果超出 [0, 1];只用于非负数据。
  • TwoSlopeNorm 要求 vmin < vcenter < vmax,越界报 must be in ascending order。
  • BoundaryNorm 返回整数索引,必须配 resampled(N) 的离散 colormap,否则整片同色。
  • ncolors 必须等于(或小于)resampled 的级数,否则最后几级用不到。
  • CenteredNorm 与 TwoSlopeNorm 都是「0 对中间色」,但前者自动取对称范围、后者让你手动给两端。

colorbar():色条

colorbar 是「把 ScalarMappable 的映射关系画成一根标尺」。它是 Figure 的方法(fig.colorbar),不是 Axes 的——因为色条本身要占一块独立的 axes。

Python19 行
Figure.colorbar(
    mappable,               # ★ 带 cmap+norm 的对象:imshow/scatter/contourf 的返回值
    cax=None,               # ★ 给色条专用的 axes;给了就完全手动
    ax=None,                # ★ 从这些 axes 里「抢」空间;可以传 list
    use_gridspec=None,      # True 时用 gridspec 切,配合 constrained_layout
    location=None,          # 'left' / 'right' / 'top' / 'bottom'
    orientation=None,       # 'vertical' / 'horizontal'
    fraction=0.15,          # 从 ax 抢走多大比例
    shrink=1.0,             # 色条长度缩放
    aspect=20,              # 色条长宽比
    pad=0.05,               # 与主图的间距
    anchor=(0.0, 0.5),      # 色条在分配框里的锚点
    extend="neither",       # 'neither'/'both'/'min'/'max',两端要不要三角
    extendfrac=None,        # 三角占多长
    ticks=None,             # 刻度位置
    boundaries=None,        # 分段边界(配 BoundaryNorm)
    spacing='uniform',      # 'uniform' / 'proportional'
    label="",               # 色条标题
)
参数 一句话 默认值的影响
mappable 谁的颜色 必填;contourf 返回的 QuadContourSet 也行
cax 手动指定色条 axes None;与 ax 同给时 cax 生效
ax 从哪些 axes 抢空间 None 时必须有 cax
fraction / pad / shrink / aspect 尺寸四件套 0.15 / 0.05 / 1.0 / 20
extend 两端三角 'neither',超范围值无提示
ticks / boundaries 刻度 / 分段 自动算
spacing 分段等宽还是等比例 'uniform'
location / orientation 放哪边 / 横竖 跟 ax 自动推

最小示例

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

fig, ax = plt.subplots()
im = ax.imshow(np.arange(144).reshape(12, 12), cmap='viridis')
cb = fig.colorbar(im, ax=ax, label='得分')
print(cb.vmin, cb.vmax)
# 输出: 0.0 143.0
plt.show()

colorbar 的三种放法与 extend

ax= 与 cax=:自动挤压 vs 手动定位

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

fig, ax = plt.subplots()
im = ax.imshow(np.arange(25).reshape(5, 5))

# ① ax=:从主图身上抢走 15% 宽度,主图被压窄
cb = fig.colorbar(im, ax=ax)

# ② cax=:完全手动,主图尺寸不变
cax = ax.inset_axes([1.04, 0.05, 0.04, 0.9])
cb2 = fig.colorbar(im, cax=cax)
plt.show()

ax= 的代价是主图被压窄——图变窄后所有文字相对变大,排版全变。要「紧贴主图且不压缩主图」,只能用 cax= 自己切一块。

两个都给不会报错,cax 静默生效:

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

fig, ax = plt.subplots()
im = ax.imshow(np.arange(25).reshape(5, 5))
cax = fig.add_axes([1.02, 0, .05, 1])
cb = fig.colorbar(im, ax=ax, cax=cax)
print(cb.ax is cax)
# 输出: True

但 cax 只接受单个 mappable,传 list 会失败:

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

fig, ax = plt.subplots()
im = ax.imshow(np.arange(25).reshape(5, 5))
try:
    fig.colorbar([im, im], cax=fig.add_axes([1.02, 0, .05, 1]))
except AttributeError as e:
    print(e)
# 输出: 'list' object has no attribute 'cmap'

尺寸四件套:fraction / pad / shrink / aspect

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

fig, ax = plt.subplots()
im = ax.imshow(np.arange(25).reshape(5, 5))
cb = fig.colorbar(im, ax=ax, fraction=.08, pad=.02, shrink=.9, aspect=25)
plt.show()
  • fraction:从主图抢走多少宽度(只在 ax= 模式下有效)
  • pad:色条与主图之间留多少(同样是主图宽度的比例)
  • shrink:色条自身长度缩放(0.6 就是只占六成长度)
  • aspect:色条长宽比,越大越细长

四个都是相对比例,不是像素。cax= 模式下这四个全部失效——尺寸由 inset_axes / add_axes 的矩形决定。

extend:超范围值要不要提示

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

fig, ax = plt.subplots()
im = ax.imshow(np.arange(25).reshape(5, 5))
cb = fig.colorbar(im, ax=ax, extend='both', extendfrac=.12)
print(cb.extend)
# 输出: both

extend 的四个值:'neither'(默认,超范围值被夹到端点色且无提示)、'both'、'min'、'max'。设置了 vmin/vmax 但数据超出去,就应该用 extend 而不是让端点色骗人。

ticks / boundaries / spacing

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

fig, ax = plt.subplots()
im = ax.imshow(np.arange(25).reshape(5, 5))
cb = fig.colorbar(im, ax=ax, ticks=[0, 12, 24], boundaries=[0, 6, 12, 18, 24])
print(cb.boundaries)
# 输出: [0, 6, 12, 18, 24]

ticks 只改刻度位置,boundaries 会真正把色条切成段。spacing='proportional' 让每段宽度按数值区间比例显示(默认 'uniform' 等宽)。

连续 colorbar 与 BoundaryNorm 分段 colorbar 的对比

事后改样式:cb.ax 就是一根普通 axes

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

fig, ax = plt.subplots()
im = ax.imshow(np.random.default_rng(0).normal(size=(8, 8)))
cb = fig.colorbar(im, ax=ax)
cb.outline.set_linewidth(0.8)
cb.ax.tick_params(labelsize=8, length=2)
cb.set_label('得分', rotation=270, labelpad=14)
cb.ax.set_title('K', fontsize=9, pad=6)
for s in cb.ax.spines.values():
    s.set_visible(False)
plt.show()

cb.ax 是真 Axes,tick_params / spines / set_title 都能用。色条标题用 cb.ax.set_title()(3.3+ 的标准做法),不要用 xaxis.set_label_position('top') 那种老写法。

返回对象
Python13 行
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
im = ax.imshow(np.arange(25).reshape(5, 5))
cb = fig.colorbar(im, ax=ax)
print(type(cb).__name__)
# 输出: Colorbar

print(cb.vmin, cb.vmax)
# 输出: 0.0 24.0
print(type(cb.ax).__name__, type(cb.solids).__name__)
# 输出: Axes QuadMesh

Colorbar 是 Artist 的子类,cb.ax 是它占的那根 axes,cb.solids 是内部的「颜色块」artist(imshow 生成的是 QuadMesh,contourf 生成的是 QuadContourSet)。

常用方法都在 Colorbar 上,不用下钻到 cb.ax:

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

fig, ax = plt.subplots()
im = ax.imshow(np.arange(25).reshape(5, 5))
cb = fig.colorbar(im, ax=ax)
cb.set_label('得分')
cb.set_ticks([0, 12, 24])
cb.set_ticklabels(['低', '中', '高'])
print([t.get_text() for t in cb.ax.get_yticklabels()])
# 输出: ['低', '中', '高']

移除用 cb.remove();cb.minorticks_on() 打开次刻度;cb.add_lines(levels, colors) 在色条上叠加等值线位置——这个在做 contourf 图时很有用。

注意事项

  • ax= 会压窄主图;要主图不变大必须用 cax=。
  • ax= 与 cax= 同给不会报错,cax 静默胜出(实测 cb.ax is cax 为 True)。
  • cax= 只接受单个 mappable,传 list 报 'list' object has no attribute 'cmap'。
  • cax= 模式下 fraction / pad / shrink / aspect 全部失效。
  • 多个图共用一条色条:先给所有 mappable set_norm(同一个 norm),再 fig.colorbar(s1, ax=[ax1, ax2])。
  • extend 默认 'neither',超范围值被安静地夹到端点色——这会让极值看起来和次极值同色。
  • cb.ax.set_title() 才是加色条标题的正确方式,set_xlabel 在竖色条上不会显示在上方。
  • BoundaryNorm 配 colorbar 时必须同时给 ticks 和 boundaries,否则刻度和分段对不上。

综合示例:三种放法 / 共享色条 / 分段色条

Python34 行
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(1)
Z = rng.normal(size=(14, 14))
fig, axs = plt.subplots(1, 3, figsize=(13.5, 3.6))

# ① ax= 自动挤压
ax = axs[0]
im = ax.imshow(Z, cmap='viridis')
fig.colorbar(im, ax=ax, label='自动挤压')
ax.set_title('ax=', fontsize=9)

# ② cax= 精确控制
ax = axs[1]
im = ax.imshow(Z, cmap='magma')
cax = ax.inset_axes([1.04, 0.05, 0.04, 0.9])
cb = fig.colorbar(im, cax=cax)
cb.set_label('手动', rotation=270, labelpad=12)
ax.set_title('cax=', fontsize=9)

# ③ 分段 + extend
ax = axs[2]
bounds = [-2.5, -1.5, -0.5, 0.5, 1.5, 2.5]
cmap = mpl.colormaps['coolwarm'].resampled(len(bounds) - 1)
norm = mpl.colors.BoundaryNorm(bounds, cmap.N)
im = ax.imshow(np.clip(Z, -3, 3), cmap=cmap, norm=norm)
fig.colorbar(im, ax=ax, ticks=bounds, boundaries=bounds,
             spacing='proportional', extend='both')
ax.set_title('BoundaryNorm + extend', fontsize=9)

plt.tight_layout()
plt.show()

第 ③ 格的 np.clip(Z, -3, 3) 是必须的:BoundaryNorm 遇到超出 bounds 的值会返回 -1 或 ncolors 这种非法索引(实测 [-5, -2, 0, 2, 5] → [-1, 0, 2, 4, 4],而合法索引只有 0~3),结果是越界值全部画成同一色甚至渲染出错。配合 extend 才显示成三角形;不裁剪又不给 extend 就会出现「明明有极值但颜色一样」。

自定义 colormap

内置色图不够用时自己造。两条路:LinearSegmentedColormap(在关键色之间插值)和 ListedColormap(一组离散色,不插值)。

Python11 行
matplotlib.colors.LinearSegmentedColormap.from_list(
    name,                   # 色图名,随便取
    colors,                 # ['#...', ...] 或 [(位置, '#...'), ...]
    N=256,                  # 采样级数
)
matplotlib.colors.ListedColormap(
    colors,                 # 颜色列表
    name="from_list",       # 名字
    N=None,                 # 级数,默认等于颜色数
)
matplotlib.colormaps['Blues'].resampled(6)     # 连续 → 离散
参数 一句话 默认值的影响
colors 关键色 必填;给元组可控制位置
(位置, 色) 形式 每色的锚点(0~1) 不给则等距分布
N 采样级数 256,够平滑
from_list(name=...) 色图名 会注册到全局?不会,只在返回值里

最小示例

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

cmap = mpl.colors.LinearSegmentedColormap.from_list(
    'brand', ['#0d3b66', '#faf0ca', '#f4d35e', '#ee964b', '#f95738'])
print(cmap.N)
# 输出: 256

fig, ax = plt.subplots()
ax.imshow(np.linspace(0, 1, 256).reshape(1, -1), aspect='auto', cmap=cmap)
plt.show()

from_list 的锚点位置决定分布,Listed 不插值

等距 vs 显式锚点

Python13 行
import matplotlib as mpl

even = mpl.colors.LinearSegmentedColormap.from_list(
    'even', ['#2c7bb6', '#f7f7f7', '#d7191c'])

explicit = mpl.colors.LinearSegmentedColormap.from_list(
    'explicit', [(0, '#2c7bb6'), (.5, '#f7f7f7'), (1, '#d7191c')])

skewed = mpl.colors.LinearSegmentedColormap.from_list(
    'skewed', [(0, '#2c7bb6'), (.12, '#f7f7f7'), (1, '#d7191c')])

print(even.N, explicit.N, skewed.N)
# 输出: 256 256 256

三者的 N 都一样,差别只在中点锚在哪儿:第三个把白色锚在 .12,蓝色那一侧被压扁,整张图偏红。做一个「0 附近要特别敏感」的发散色图,就是靠这个。

ListedColormap:不插值

Python5 行
import matplotlib as mpl

lc = mpl.colors.ListedColormap(['#003f5c', '#58508d', '#bc5090', '#ff6361', '#ffa600'])
print(lc.N)
# 输出: 5

lc.N 等于颜色数,lc(0.3) 会返回第 1 个颜色而不是插值结果——这就是「离散分级填色」要的效果。

bad / under / over:三个特殊值

Python8 行
import matplotlib as mpl

lc = mpl.colors.ListedColormap(['#003f5c', '#58508d', '#bc5090'])
lc.set_bad('k')
lc.set_under('darkblue')
lc.set_over('purple')
print(mpl.colors.to_hex(lc.get_bad()), mpl.colors.to_hex(lc.get_under()))
# 输出: #000000 #00008b
  • bad:NaN / 被掩码的值
  • under:小于 vmin 的值(需 norm.clip=False)
  • over:大于 vmax 的值

这三个颜色只在色图上生效,不会出现在色条上(除非配 extend)。

3.10 的 ListedColormap 不接受 bad= 关键字,必须事后 set_bad:

Python7 行
import matplotlib as mpl

try:
    mpl.colors.ListedColormap(['r', 'g', 'b'], bad='k')
except TypeError as e:
    print(e)
# 输出: ListedColormap.__init__() got an unexpected keyword argument 'bad'

resampled:把连续色图离散成 N 级

Python5 行
import matplotlib as mpl

cm6 = mpl.colormaps['Blues'].resampled(6)
print(cm6.N)
# 输出: 6

它返回的是新的 ListedColormap,原色图不受影响。这是做「分级填色」的标准做法:resampled(N) + BoundaryNorm(bounds, N)。

注意事项

  • from_list 的 N 默认 256;给 N 太小(如 5)会看出台阶,除非就是要离散效果。
  • 显式锚点必须从 0 开始、到 1 结束,否则两端会被拉伸填充。
  • 锚点位置不是等距时,颜色分布就偏了——这正是做「敏感区间」的手法。
  • 3.10 的 ListedColormap 没有 bad= / under= / over= 构造参数,用 set_bad() / set_under() / set_over()。
  • 3.10 的 Colormap 没有 with_alpha()(3.11 新增);要改透明度得在 to_rgba 之后手动调 alpha。
  • 自定义色图不会自动注册到 plt.colormaps(),只能靠返回值传参。
  • ListedColormap 配连续 Normalize 也能用,但边界会出现硬跳变——那正是离散色图的用途。

综合示例:品牌发散色图 / 分级色图 / 带 bad 的掩码

Python38 行
import matplotlib as mpl
from matplotlib.colors import LinearSegmentedColormap, ListedColormap
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(3)
Z = rng.normal(size=(14, 14))
fig, axs = plt.subplots(1, 3, figsize=(13.5, 3.6))

# ① 品牌发散色图:中点锚在 .5,两端不等距
div = LinearSegmentedColormap.from_list(
    'brand_div', [(0, '#0d3b66'), (.45, '#faf0ca'), (.55, '#faf0ca'), (1, '#f95738')])
ax = axs[0]
im = ax.imshow(Z, cmap=div, norm=mpl.colors.CenteredNorm())
fig.colorbar(im, ax=ax, label='相对 0')
ax.set_title('品牌发散色图', fontsize=9)

# ② 离散分级
bounds = [-2, -1, 0, 1, 2]
cmap = ListedColormap(['#2c7bb6', '#abd9e9', '#ffffbf', '#fdae61', '#d7191c'])
norm = mpl.colors.BoundaryNorm(bounds, cmap.N)
ax = axs[1]
im = ax.imshow(np.clip(Z, -2.5, 2.5), cmap=cmap, norm=norm)
fig.colorbar(im, ax=ax, ticks=bounds, boundaries=bounds, spacing='proportional')
ax.set_title('ListedColormap 分级', fontsize=9)

# ③ 带 bad:NaN 单独上色
ax = axs[2]
Z2 = Z.copy()
Z2[np.abs(Z2) < .5] = np.nan
cm3 = mpl.colormaps['coolwarm'].copy()
cm3.set_bad('#4d4d4d')
im = ax.imshow(Z2, cmap=cm3, norm=mpl.colors.CenteredNorm())
fig.colorbar(im, ax=ax, label='|x| < 0.5 为灰')
ax.set_title('set_bad 处理 NaN', fontsize=9)

plt.tight_layout()
plt.show()

第 ③ 格的关键是 cmap = ...copy():直接改 mpl.colormaps['coolwarm'] 会污染全局注册表,后面所有用 coolwarm 的图都会带着这个灰。.copy() 出的是独立副本。