文章/Matplotlib/第 27 章
颜色、色图与 colorbar
Matplotlib · 第 27 / 43 章
颜色表示方式
matplotlib 里「一个颜色」有九种合法写法,全部由 matplotlib.colors.to_rgba() 统一解析成 (r, g, b, a) 四元组。记住这一条就能解释所有「颜色参数不生效」的问题:任何接颜色的地方,最终都过这个函数。
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' |
完全透明;区别见下方注意事项 |
最小示例
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
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'])
# 输出: #DC143CCSS4_COLORS 的值是十六进制字符串,不是元组;TABLEAU_COLORS 的键里带冒号('tab:blue'),所以写 'tab.blue' 会报错。
灰度必须写成字符串
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' 不合法
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' 能被解析,但不在命名色表里
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 Falseto_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 色。
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 是全局 |
最小示例
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 色与全局改法
import matplotlib as mpl
cols = mpl.rcParams['axes.prop_cycle'].by_key()['color']
print(len(cols))
# 输出: 10
print(cols[0])
# 输出: #1f77b4by_key()['color'] 拿到的已经是十六进制字符串(不是元组),可以直接 to_hex 之外的场合复用。
全局改 rcParams 影响之后新建的所有图;ax.set_prop_cycle() 只影响这个 axes 之后画的 artist。
'C0' 不能进 cycle:这是循环引用
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
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,得先取出具体色值:
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 上不存在(见下方版本差异)。
同时轮换颜色与线型
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()+ 是逐项配对(红配实线、绿配虚线……),要求两边长度相等;长度不等直接报错:
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 种组合:
from cycler import cycler
print(len(cycler(color=['r', 'g', 'b']) * cycler(linestyle=['-', '--', ':'])))
# 输出: 9所以「颜色和线型都要轮换、但只想画 3 条线」用 +;「穷举所有组合」用 *。
set_prop_cycle 只管之后的 artist
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 上什么没有
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'))
# 输出: Falseokabe_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 |
强行转一维 |
import matplotlib.pyplot as plt
print(len(plt.colormaps()))
# 输出: 180
print(sum(1 for n in plt.colormaps() if n.endswith('_r')))
# 输出: 90
三类的判据
- 定性(qualitative):类别之间没有大小关系,色图应该「越不连续越好」。
tab10是最常用的 10 色。 - 顺序(sequential):单变量从低到高,色图应该「亮度单调」,这样黑白打印也能读。
- 发散(diverging):以某个中点(通常是 0)向两边发散,中点应该是浅色(白/灰)。
_r 后缀 = 反向
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 当调色板用
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()
import matplotlib as mpl
print(mpl.colormaps['Blues'].resampled(6).N)
# 输出: 6resampled(N) 把连续色图压成 N 级,配合 BoundaryNorm 做分段填色。
注意事项
jet/rainbow不是感知均匀的,会在中间伪造出「亮带」,正式图表别用。- 顺序色图要选亮度单调的(
viridisBluesGreys),否则灰度打印读不出高低。 - 发散色图的中段必须是浅色,否则「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 选错。
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() 做分级填色 |
最小示例
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 默认不定范围
import matplotlib as mpl
n = mpl.colors.Normalize()
print(n.vmin, n.vmax)
# 输出: None Nonevmin/vmax 都是 None 时,会在第一次绘制时按数据自动定范围。这就是「改数据范围后颜色跟着变」的原因——要固定就显式给两个值。
clip=True 把超范围的值夹到两端(默认 False 时超范围值在 imshow 里仍会被夹,但 cmap() 直接调用会得到端点色的重复):
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:负值不报错,直接变透明
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:正负都有时的正解
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 时负值会出负数
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:中心值必须在范围内
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 为中心,自动取对称范围
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
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:
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。
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 自动推 |
最小示例
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()
ax= 与 cax=:自动挤压 vs 手动定位
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 静默生效:
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 会失败:
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
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:超范围值要不要提示
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)
# 输出: bothextend 的四个值:'neither'(默认,超范围值被夹到端点色且无提示)、'both'、'min'、'max'。设置了 vmin/vmax 但数据超出去,就应该用 extend 而不是让端点色骗人。
ticks / boundaries / spacing
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' 等宽)。

事后改样式:cb.ax 就是一根普通 axes
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') 那种老写法。
返回对象
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 QuadMeshColorbar 是 Artist 的子类,cb.ax 是它占的那根 axes,cb.solids 是内部的「颜色块」artist(imshow 生成的是 QuadMesh,contourf 生成的是 QuadContourSet)。
常用方法都在 Colorbar 上,不用下钻到 cb.ax:
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,否则刻度和分段对不上。
综合示例:三种放法 / 共享色条 / 分段色条
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(一组离散色,不插值)。
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=...) |
色图名 | 会注册到全局?不会,只在返回值里 |
最小示例
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()
等距 vs 显式锚点
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:不插值
import matplotlib as mpl
lc = mpl.colors.ListedColormap(['#003f5c', '#58508d', '#bc5090', '#ff6361', '#ffa600'])
print(lc.N)
# 输出: 5lc.N 等于颜色数,lc(0.3) 会返回第 1 个颜色而不是插值结果——这就是「离散分级填色」要的效果。
bad / under / over:三个特殊值
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 #00008bbad:NaN/ 被掩码的值under:小于vmin的值(需norm.clip=False)over:大于vmax的值
这三个颜色只在色图上生效,不会出现在色条上(除非配 extend)。
3.10 的 ListedColormap 不接受 bad= 关键字,必须事后 set_bad:
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 级
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 的掩码
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() 出的是独立副本。