引言
图表设计中最容易被直觉误导的地方,就是「我觉得这样很清楚」。设计者盯着自己做的图看了一百遍,当然清楚;但读者只有三秒。视觉感知原理的价值在于,它把「清楚」从主观判断变成可测量的量:通道编码精度、颜色感知均匀性、前注意特征数量、对比度比值,都有实验数据支撑。
本文要解决三个具体问题。第一,为什么同样的数值用长度编码比用颜色编码更容易读准,误差能差多少倍。第二,为什么异常值能被「一眼看到」——这背后的前注意加工机制决定了设计上限。第三,为什么在 sRGB 里均匀取值的色阶,人眼看着是不均匀的,以及 CIELAB、OKLCH 这些感知均匀色彩空间如何解决它。
工程落地的难点集中在色彩环节。sRGB 是设备相关的非线性空间,直接对它做插值会得到明度跳变的色阶;HSL 虽然直观,但它的明度(L)与感知明度不成正比,蓝色在 L=50% 时看起来比黄色暗得多。理解这些差异,才能解释为什么 Matplotlib 从 jet 切换到 viridis 是必要的升级,而不是审美偏好。
目录
- 视觉系统的加工通路
- 通道编码精度的实验证据
- 前注意加工与视觉突显
- 颜色感知与色彩空间
- 分类、连续与发散色板设计
- 色盲友好与对比度合规
- 格式塔原则的工程应用
- 动画的感知边界
- 认知负荷与信息密度
- 工作记忆与组块策略
- 感知原理驱动的交互设计
- 可视化质量的度量方法
1. 视觉系统的加工通路
光线进入视网膜后,信号分两条通路向上传递。小细胞通路(parvocellular)负责细节与颜色,空间分辨率高但时间响应慢;大细胞通路(magnocellular)负责运动与亮度对比,时间响应快但分辨率低。这条生理事实解释了两个现象:快速运动的元素看不清细节,以及纯色相差异(等亮度)在小尺寸下难以分辨。
视觉皮层对刺激的加工是分层的。V1 处理边缘与朝向,V2/V4 处理颜色与形状,V5/MT 处理运动。前注意阶段(preattentive)发生在约 200 到 250 毫秒内,是并行的、全视野的;注意阶段(attentive)是串行的,每次只能处理一个位置,耗时随元素数量线性增长。这就是「一眼看到」与「逐个找」的生理基础。
对可视化的直接推论是:如果目标信息能用前注意特征编码,读者的搜索时间与元素数量无关。一张 200 个点的散点图,若把异常点染成红色,读者瞬间找到;若用同样的灰色、只靠位置差异,读者需要逐个扫描。设计的目标就是尽可能把关键信息放进前注意通道。
视野的分布也不均匀。中央凹(fovea)覆盖约 2 度视角,提供最高的空间分辨率与颜色辨别力;周边视野分辨率骤降但运动敏感度高。这解释了两个图表设计事实:其一,需要精确比较的数值应该放在视野中央区域,也就是仪表盘的黄金位置(左上到中部);其二,运动(动画)能吸引周边视野的注意,所以自动轮播的大屏在切换时会强行夺走读者的注意力——如果切换太频繁,读者无法在任何一张图上完成阅读。
1.1 亮度与颜色通道的优先级
在信息编码的优先级上,亮度高于颜色。原因在于亮度通道的空间分辨率远高于颜色通道——视网膜上处理亮度的细胞数量是处理颜色的数倍。因此,用亮度编码的数据能在更小的尺寸、更远的距离上被读出,而颜色编码的数据一旦缩小到几个像素就糊成一团。
这条规律的直接应用是:大屏和移动端优先用亮度差异,其次才是色相差异。同样一组分类数据,如果屏幕远、元素小,用深浅不同的蓝色比用红绿蓝三种色相更可靠。反过来,如果需要在一屏内区分 8 个类别,颜色色相是唯一够用的通道,但要接受「远看分不清」的代价。
2. 通道编码精度的实验证据
Cleveland 与 McGill 在 1984 年做了一组经典实验:让被试判断两个编码值之间的比例关系,测量判断误差。结果给出的精度排序被后续大量研究复现:
| 通道 | 相对精度 | 典型误差 | 适用数据 |
|---|---|---|---|
| 位置(公共基线) | 最高 | ~1.4% | 定量 |
| 位置(非公共基线) | 高 | ~2.5% | 定量 |
| 长度 | 高 | ~3.5% | 定量 |
| 角度/斜率 | 中 | ~5% | 定量 |
| 面积 | 中低 | ~8% | 定量 |
| 体积 | 低 | ~12% | 定量 |
| 颜色明度 | 低 | ~15% | 定序 |
| 颜色色相 | 最低 | >20% | 定类 |
这张表直接给出了设计规则:需要精确读数的量用位置或长度编码(条形图、折线图),只需要看大致量级或分类的量才用面积和颜色。它同时解释了为什么饼图(角度)和气泡图(面积)不适合精确比较。
「公共基线」这一行值得注意:同一基线上比较多个条形,精度比各自独立的两个条形更高,因为眼睛可以直接比较长度差。这解释了为什么分组柱状图优于两张独立的单柱图——但前提是组内共享零基线。
3. 前注意加工与视觉突显
前注意特征(preattentive features)是能在 200 毫秒内被并行检测的视觉属性。经典清单包括:颜色色相、朝向、大小、形状、运动、空间位置、密度、闪烁、闭合性、线段长度与宽度。
关键限制是特征数量。当一张图只用一种前注意特征区分目标时,搜索是并行的(目标「弹出」);当需要组合两个特征(如「红色的圆」在「红色方块和蓝色圆」中)时,搜索退化为串行,时间随元素数量线性增长,这叫联结搜索(conjunctive search)。
对可视化的落地含义有三条:
// 反例:用两个前注意特征做联结搜索,读者必须逐个检查
// 想找「华东区 + 已超期」的柱子,需要同时判断颜色和纹理
series: [{ type: 'bar', itemStyle: { color: '#5470c6', decal: 'line' } }]
// 正例:把「已超期」用单一特征编码,并把其他柱子去饱和
series: [{
type: 'bar',
data: data.map((v, i) => ({
value: v,
// 只有超期项保留高饱和色,其余压成灰色,异常瞬间弹出
itemStyle: { color: overdue[i] ? '#d03050' : '#d8d8d8' },
})),
}]
第一条规则是减少需要同时区分的前注意特征数量,能用一个就别用两个。第二条是对比度要够,前注意检测依赖特征差异的绝对幅度,淡红与淡灰之间的差异不足以触发弹出。第三条是给非重点元素降饱和——把背景元素统一压成中性灰,让目标元素成为视野里唯一的高饱和项。
4. 颜色感知与色彩空间
sRGB 是绝大多数显示器的工作空间,但它是设备相关的非线性空间:三个通道的值经过 gamma 编码,等量的数值变化不对应等量的感知变化。直接对 sRGB 三元组做线性插值,得到的中间色在感知上偏亮或偏暗。
常见的几个色彩空间各有定位:
| 空间 | 模型 | 感知均匀 | 用途 |
|---|---|---|---|
| sRGB | 非线性 RGB | 否 | 设备输出 |
| linear RGB | 线性 RGB | 否 | 光照计算、正确混合 |
| HSL/HSV | 柱坐标 | 否 | 直观调色,非感知均匀 |
| CIELAB | 笛卡尔 Lab* | 是 | 色差计算 ΔE |
| CIELCh | 极坐标 LCh | 是 | 色阶插值 |
| OKLCH | 改进的 Lab 变体 | 是(更优) | 现代色板设计 |
HSL 的陷阱最值得展开:它的 L 通道定义为 (max+min)/2,与感知明度无关。hsl(60, 100%, 50%)(黄)和 hsl(240, 100%, 50%)(蓝)的 L 相同,但黄的感知明度远高于蓝。用 HSL 生成「同明度不同色相」的分类色板,结果会是一组明暗差异巨大的颜色,其中某些在浅色背景上完全不可读。
OKLCH 是当前推荐的选择,它的 L 通道直接对应感知明度,且色域覆盖优于 CIELAB:
/* OKLCH: 感知均匀的色相环,固定 L 与 C 只变 H */
.palette {
--c1: oklch(0.62 0.18 25); /* 红 */
--c2: oklch(0.62 0.18 145); /* 绿 */
--c3: oklch(0.62 0.18 250); /* 蓝 */
--c4: oklch(0.62 0.18 320); /* 紫 */
}
这四个颜色在感知明度上几乎一致,作为分类色板不会出现「某个颜色更抢眼」的问题。
需要区分「感知均匀」与「色域边界」两件事。OKLCH 允许指定超出 sRGB 色域的颜色,浏览器会自动裁剪(gamut mapping),裁剪后的实际颜色可能偏离原值。工程上应在生成色板后用 colorjs.io 或 culori 检查每个颜色是否在 sRGB 内:
import { converter, inGamut } from 'culori';
const toRgb = converter('rgb');
const candidates = ['oklch(0.62 0.18 25)', 'oklch(0.62 0.18 145)'];
for (const c of candidates) {
const rgb = toRgb(c);
const inside = inGamut('rgb')(c);
console.log(c, 'in-sRGB:', inside, rgb);
}
如果某个色相在目标明度下超出色域(通常是饱和度需求较高的青色和品红),有两种处理:降低该色的 chroma,或整体抬高/降低明度。不要依赖浏览器裁剪,因为不同浏览器的裁剪算法不同,会导致跨平台色板不一致。
4.1 颜色的混合必须在正确的空间进行
半透明叠加、渐变、抗锯齿边缘的颜色混合,理论上应在线性 RGB 空间进行。在 sRGB 空间直接做 alpha 混合,会让叠加区域偏暗或偏亮。浏览器默认在 sRGB 空间做合成,但 CSS Color 4 允许显式指定:
.overlay {
/* 在线性 RGB 空间做混合,避免 sRGB 合成的亮度偏移 */
mix-blend-mode: multiply;
color-interpolation: linearRGB;
}
对图表而言,这个细节在两种情况会明显暴露:一是用半透明散点叠加表示密度时,混合结果的整体明度会偏离预期;二是用渐变填充面积图时,sRGB 插值的中间色偏暗。Canvas 的 createLinearGradient 就是 sRGB 插值,需要更精确时得手动用 d3-interpolate 的 interpolateLab 或 interpolateRgbBasis 生成色标。
5. 分类、连续与发散色板设计
三类色板对应三类数据,用错是高频错误。
分类色板(categorical)用于定类数据,目标是任意两色可区分且感知明度接近。色相数量上限约 8 到 10 个,超过后色相环上没有足够间隔,人眼开始混淆。超过 10 类时应该考虑换图表类型(如条形图 + 直接标注),而不是硬造更多颜色。
连续色板(sequential)用于定量数据,目标是明度单调递增。它是唯一允许「明度单调」的色板类型,因为明度本身就是编码通道。Viridis、Magma、Cividis 都是感知均匀的连续色板;Jet/Rainbow 不是——它的明度在中段有回折,会产生虚假的边界带。
发散色板(diverging)用于有自然中点的数据(如增长率正负、温度偏离均值),两端用不同色相、中点用浅色或白色。关键约束是中点必须对应数据的有意义零点,不能随便取均值——如果零点在数据分布中偏在一侧,发散色板会误导读者以为两侧对称。
import numpy as np
from matplotlib.colors import LinearSegmentedColormap
# 反例:jet 在感知上非均匀,中段有虚假边界
bad = 'jet'
# 正例 1:viridis 明度单调,适合连续量
good_seq = 'viridis'
# 正例 2:自定义发散色板,中点用近白色
div = LinearSegmentedColormap.from_list(
'rd_bu', ['#67001f', '#d6604d', '#f7f7f7', '#4393c3', '#053061'])
values = np.random.default_rng(0).normal(0, 1, (12, 12))
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 2, figsize=(10, 4.2))
axes[0].imshow(values, cmap=bad); axes[0].set_title('jet (avoid)')
axes[1].imshow(values, cmap=div, vmin=-2, vmax=2); axes[1].set_title('diverging (ok)')
plt.tight_layout(); plt.savefig('palette.png', dpi=140)
发散色板的 vmin/vmax 必须对称(如 -2 与 +2),否则零点会偏离视觉中点。
6. 色盲友好与对比度合规
红绿色盲(protanopia/deuteranopia)在男性中约占 8%,是最主要的无障碍约束。红绿组合对这类人群不可区分,而它恰好是财务图表里最常用的「亏损红/盈利绿」。
Okabe-Ito 色板是公认的色盲安全分类色板,八个颜色在两两之间都能被各类色觉缺陷区分:
| 名称 | HEX | 感知明度 |
|---|---|---|
| Black | #000000 | 极低 |
| Orange | #E69F00 | 高 |
| Sky Blue | #56B4E9 | 高 |
| Bluish Green | #009E73 | 中 |
| Yellow | #F0E442 | 极高 |
| Blue | #0072B2 | 中 |
| Vermillion | #D55E00 | 中 |
| Reddish Purple | #CC79A7 | 中 |
注意这张表里明度跨度很大,因此不能用它做「同明度分类色板」——它的设计目标是色觉缺陷下的最大可区分性,代价是明度不齐。如果色板同时需要色盲安全和明度一致,需要自己用 OKLCH 重新调。
对比度方面,文本与背景要达到 WCAG AA 的 4.5:1(大字号 3:1)。图表元素如坐标轴、网格线属于「图形对象」,AA 要求 3:1。工程上可以用相对亮度公式直接计算:
function relativeLuminance(hex) {
const c = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255)
.map((v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4));
return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
}
function contrastRatio(a, b) {
const [l1, l2] = [relativeLuminance(a), relativeLuminance(b)].sort((x, y) => y - x);
return (l1 + 0.05) / (l2 + 0.05);
}
console.log(contrastRatio('#5470c6', '#ffffff').toFixed(2)); // 需 >= 3
这套无障碍计算与前端工程里的语义化实践是同一套标准,可以参考 前端无障碍与国际化实践 中的 WCAG 落地细节。
7. 格式塔原则的工程应用
格式塔(Gestalt)原则描述了感知如何自动分组,它决定了图表的默认读法。
接近性(proximity):距离近的元素被视为一组。分组柱状图里组内间距必须小于组间距,通常取 1:3 的比例。如果等距,读者无法判断哪根柱属于哪一组。
相似性(similarity):颜色、形状相同的元素被归为一组。这就是为什么同一系列在所有子图中必须用同一颜色——颜色语义一旦不一致,读者会把不同系列的线连起来读。
连通性(connectedness):用线条连接的元素被视为一个整体,这是折线图有效的机制。连通性比接近性和相似性更强,所以折线图里不同序列即使颜色相近也能被追踪。
共同区域(common region):被同一背景框包围的元素被视为一组,这是仪表盘用卡片分区的依据。
连续性(continuity):眼睛倾向于沿平滑路径追踪,这是为什么折线图的拐点比柱状图的顶部更容易追踪。
// 接近性:组内间距 10%,组间间距 60%
series: [
{ name: 'A', type: 'bar', barGap: '10%', barCategoryGap: '60%' },
{ name: 'B', type: 'bar' },
]
8. 动画的感知边界
动画能帮助读者建立「前后状态」的对应关系,但它也受感知限制。变化盲视(change blindness)说明,如果两个状态之间没有可见的连续过渡,读者可能完全注意不到变化——这就是为什么筛选条件变化时应该做过渡动画而不是瞬间重绘。
但有三个硬约束。其一,时长:150 到 300 毫秒是感知上的「立即」,超过 500 毫秒读者会感觉卡顿,超过 1 秒会打断注意力。其二,帧率:低于 30fps 的动画看起来是跳跃的,60fps 是流畅基线。其三,缓动:线性运动在视觉上显得机械,ease-out 更符合物理直觉。
// ECharts 过渡动画配置
option = {
animationDuration: 300, // 进入动画
animationDurationUpdate: 200, // 数据更新动画,应更短
animationEasing: 'cubicOut',
animationEasingUpdate: 'cubicInOut',
// 大量数据时关闭动画,避免掉帧
animation: data.length < 500,
};
减少动效偏好必须被尊重。系统级设置 prefers-reduced-motion 应被检测并关闭动画:
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
9. 认知负荷与信息密度
认知负荷分三类:内在负荷(数据本身的复杂度,不可消除)、外在负荷(由设计不当引入的额外负担,应消除)、相关负荷(用于建立图式的心智投入,应保留)。
外在负荷的来源很具体:多余的图例(可以改为直接标注)、重复的单位(每个刻度都写「万元」)、需要来回对照的分离式图例、没有对齐的多个小图。消除它们的收益是线性的——每减少一项,读者就少做一次工作记忆的搬运。
信息密度的衡量可以用 Tufte 的数据墨水比:编码数据的墨水占总墨水的比例。理想接近 1。常见稀释项包括:3D 效果、渐变填充、阴影、背景网格(可以改成极浅的参考线)、装饰性边框。
但数据墨水比不应绝对化。一个被广泛引用的反例是 sparkline(迷你趋势线)——它故意去除坐标轴和标签,牺牲可读的精确值来换取极高的信息密度,在仪表盘的小卡片里非常有效。判断标准是使用场景:需要精确读数时保留刻度,只需要看趋势时可以去刻度。
10. 工作记忆与组块策略
工作记忆的容量是 4±1 个组块(Cowan 的现代估计,比 Miller 的 7±2 更严格)。这个数字直接约束了图表设计。
系列数上限:一张图里能被同时跟踪的序列不超过 4 到 5 条。超过后读者必须依赖图例反复对照,每次对照都占用工作记忆,很快溢出。
图例位置:图例与数据分离会强制工作记忆搬运。最优解是直接标注(direct labeling)——把系列名写在曲线末端,读者无需在空间上跳跃。
数字精度:1,234,567.89 需要读者在心里做组块,123万 或 1.2M 只需一次读取。大屏和仪表盘的数字应做量级简化。
颜色语义:同一颜色在同一屏内只能有一个含义。如果红色在一张卡里表示「告警」,在另一张里表示「华东区」,读者每次都要重新建立映射,认知成本累积。
// 直接标注优于图例:把系列名放到曲线末端
series: [{
type: 'line',
endLabel: {
show: true,
formatter: '{a}', // {a} 是系列名
fontSize: 12,
color: '#333',
},
data: seriesData,
}]
11. 感知原理驱动的交互设计
把感知结论转成交互规则,有五条可以直接落地。
悬停提示要近且稳:tooltip 出现在鼠标旁 8 到 12 像素处,跟随时有延迟(约 100 毫秒)以避免抖动,且不能遮挡当前数据点。
高亮要改变前注意特征:鼠标悬停某序列时,把它加粗并提高不透明度,同时把其他序列压到 20% 不透明度。这样目标在视野里瞬间弹出,而不是让读者逐条比较。
筛选要即时反馈:筛选条件变化后 100 毫秒内应有视觉响应,即使数据还在加载,也应先显示骨架或旧数据的淡化版本。
下钻要有路径感:点击某个柱进入明细时,用面包屑保留层级路径,让读者知道自己在哪一层。这与感知上的「共同区域」原则一致——每一层是一个区域。
键盘可达:所有交互都应有键盘等价操作,且焦点态要可见(3:1 对比度)。这既是无障碍要求,也让高级用户可以用键盘快速操作。
12. 可视化质量的度量方法
可视化质量可以被测量,而不是只靠主观评审。四种低成本方法。
五秒测试:给被试看 5 秒后移开,让他说出这张图在讲什么。如果答不出核心结论,说明标题、标注或编码有问题。
数值读取测试:随机挑三个数据点,让被试读出数值。误差超过 5% 说明编码通道精度不足或刻度设计有问题。
眼动追踪:记录注视点路径。理想情况下注视点应集中在数据区,若大量注视点落在图例区,说明需要改成直接标注。
A/B 对比:同一份数据做两个版本,测量被试得出正确结论的时间与正确率。这比任何主观评分都可靠。
工程上还有一个廉价指标:截图测试。把图表截图发给没参与制作的人,问三个问题——这张图在比较什么、最大值在哪、单位是什么。三个问题都能在 10 秒内答对,基本合格。
权衡取舍
| 决策点 | 选项 A | 选项 B | 何时选 A | 何时选 B |
|---|---|---|---|---|
| 色板空间 | sRGB 直取 | OKLCH 生成 | 快速原型 | 需要感知均匀 |
| 连续色板 | Viridis 系 | 单色相渐变 | 需要高对比细节 | 需要与品牌色一致 |
| 分类色数量 | ≤8 色相 | 超 8 类 | 常规分类 | 改用条形图+标注 |
| 色盲安全 | Okabe-Ito | 自定义明度一致板 | 只求可区分 | 还需明度一致 |
| 动画 | 开启过渡 | 关闭动画 | 数据量小、状态切换 | 数据量大或降级模式 |
| 图例 | 独立图例 | 直接标注 | 序列少、空间紧 | 序列多、末端有空间 |
| 精度 | 位置/长度 | 面积/颜色 | 需要读数 | 只需量级 |
常见坑清单
- 用 HSL 生成同明度色板——HSL 的 L 与感知明度无关,黄蓝同 L 却明暗迥异;改用 OKLCH。
- 连续量用 jet/rainbow——明度非单调产生虚假边界;改用 viridis 或单色相渐变。
- 红绿作为唯一区分——8% 男性无法区分;叠加形状或改用 Okabe-Ito。
- 发散色板零点不对称——视觉中点偏离数据零点,误导正负分布;令 vmin/vmax 对称。
- 同时用两个前注意特征——退化为串行搜索,读者逐个找;只用一个特征编码目标。
- 图例与数据分离——强制工作记忆搬运,序列多时溢出;改为末端直接标注。
- 动画时长超 500 毫秒——读者感知为卡顿;更新动画压到 200 毫秒以内。
- 忽略 prefers-reduced-motion——前庭功能敏感用户不适;提供关闭动画分支。
- 分组间距等于组内间距——格式塔接近性失效,读者配错组;组间距取组内的 3 倍。
- 网格线过深——稀释数据墨水比,干扰数据读取;网格线明度接近背景即可。
小结
视觉编码的核心结论可以压缩成一句话:通道精度决定了什么能量、什么只能看个大概。位置和长度用于需要读数的量,面积和角度用于看量级,颜色用于分类和粗粒度定序。前注意加工则决定了异常检测的上限——关键信息必须能用单一前注意特征编码,且对比度要足够触发弹出。
色彩是工程落地中最容易出错的一环。sRGB 与 HSL 都不是感知均匀空间,直接在其中插值会产生明度跳变。OKLCH 和 CIELAB 提供了感知均匀的插值基础,viridis 与 Okabe-Ito 则分别给出了连续与分类场景的现成答案。这些选择背后都有色觉生理与实验数据支撑,不是风格偏好。
下一步可以按技术栈继续:图表配置层面的落地细节见 ECharts 图表工程实战 ,多图组合时的视觉层次与布局见 仪表盘与数据大屏设计 ,如果关注图表选型的整体决策框架,回到 数据可视化原理与图表选型 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。