视觉编码与感知原理

从人眼与视觉皮层的加工机制出发,解释为什么位置编码比颜色精确、前注意加工为什么能让异常瞬间跳出、CIELAB 与 OKLCH 如何保证色阶感知均匀,并给出色板设计、色盲友好、动画感知边界的可执行规则与代码。

引言

图表设计中最容易被直觉误导的地方,就是「我觉得这样很清楚」。设计者盯着自己做的图看了一百遍,当然清楚;但读者只有三秒。视觉感知原理的价值在于,它把「清楚」从主观判断变成可测量的量:通道编码精度、颜色感知均匀性、前注意特征数量、对比度比值,都有实验数据支撑。

本文要解决三个具体问题。第一,为什么同样的数值用长度编码比用颜色编码更容易读准,误差能差多少倍。第二,为什么异常值能被「一眼看到」——这背后的前注意加工机制决定了设计上限。第三,为什么在 sRGB 里均匀取值的色阶,人眼看着是不均匀的,以及 CIELAB、OKLCH 这些感知均匀色彩空间如何解决它。

工程落地的难点集中在色彩环节。sRGB 是设备相关的非线性空间,直接对它做插值会得到明度跳变的色阶;HSL 虽然直观,但它的明度(L)与感知明度不成正比,蓝色在 L=50% 时看起来比黄色暗得多。理解这些差异,才能解释为什么 Matplotlib 从 jet 切换到 viridis 是必要的升级,而不是审美偏好。

目录

  1. 视觉系统的加工通路
  2. 通道编码精度的实验证据
  3. 前注意加工与视觉突显
  4. 颜色感知与色彩空间
  5. 分类、连续与发散色板设计
  6. 色盲友好与对比度合规
  7. 格式塔原则的工程应用
  8. 动画的感知边界
  9. 认知负荷与信息密度
  10. 工作记忆与组块策略
  11. 感知原理驱动的交互设计
  12. 可视化质量的度量方法

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自定义明度一致板只求可区分还需明度一致
动画开启过渡关闭动画数据量小、状态切换数据量大或降级模式
图例独立图例直接标注序列少、空间紧序列多、末端有空间
精度位置/长度面积/颜色需要读数只需量级

常见坑清单

  1. 用 HSL 生成同明度色板——HSL 的 L 与感知明度无关,黄蓝同 L 却明暗迥异;改用 OKLCH。
  2. 连续量用 jet/rainbow——明度非单调产生虚假边界;改用 viridis 或单色相渐变。
  3. 红绿作为唯一区分——8% 男性无法区分;叠加形状或改用 Okabe-Ito。
  4. 发散色板零点不对称——视觉中点偏离数据零点,误导正负分布;令 vmin/vmax 对称。
  5. 同时用两个前注意特征——退化为串行搜索,读者逐个找;只用一个特征编码目标。
  6. 图例与数据分离——强制工作记忆搬运,序列多时溢出;改为末端直接标注。
  7. 动画时长超 500 毫秒——读者感知为卡顿;更新动画压到 200 毫秒以内。
  8. 忽略 prefers-reduced-motion——前庭功能敏感用户不适;提供关闭动画分支。
  9. 分组间距等于组内间距——格式塔接近性失效,读者配错组;组间距取组内的 3 倍。
  10. 网格线过深——稀释数据墨水比,干扰数据读取;网格线明度接近背景即可。

小结

视觉编码的核心结论可以压缩成一句话:通道精度决定了什么能量、什么只能看个大概。位置和长度用于需要读数的量,面积和角度用于看量级,颜色用于分类和粗粒度定序。前注意加工则决定了异常检测的上限——关键信息必须能用单一前注意特征编码,且对比度要足够触发弹出。

色彩是工程落地中最容易出错的一环。sRGB 与 HSL 都不是感知均匀空间,直接在其中插值会产生明度跳变。OKLCH 和 CIELAB 提供了感知均匀的插值基础,viridis 与 Okabe-Ito 则分别给出了连续与分类场景的现成答案。这些选择背后都有色觉生理与实验数据支撑,不是风格偏好。

下一步可以按技术栈继续:图表配置层面的落地细节见 ECharts 图表工程实战 ,多图组合时的视觉层次与布局见 仪表盘与数据大屏设计 ,如果关注图表选型的整体决策框架,回到 数据可视化原理与图表选型 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「数据可视化」更多文章

  1. WebGL 与三维数据可视化
  2. 数据叙事与图表沟通
  3. 嵌入式分析与白标集成