第 3 章 色彩专题

颜色模型、色图与可访问性 · 8 个小节

Alt + ← / → 切换章节课件

3.1 颜色模型基础

为什么需要 4 种颜色空间?它们各自擅长什么?色盲眼中看到的世界是怎样的?

为什么要学颜色模型?

设计 colormap 时,不能直接用 RGB 插值——因为 RGB 不是感知均匀的(255,0,0 与 0,255,0 "看上去"亮度差很多)。你需要感知均匀的颜色空间(Lab / HCL)来插值,才能得到平滑的渐变。

4 大颜色空间

  • RGB:加色模型,设备相关。屏幕显示的默认。
  • HSL:艺术家友好(色相/饱和度/亮度独立),但仍不感知均匀。
  • CIE Lab:感知均匀,设计 colormap 的黄金标准。
  • CMYK:减色(印刷),web 可视化用不到。

CVD(色觉缺陷)

约 8% 男性、0.5% 女性是色盲。常见三种:

  • 红色盲(protanopia):约 1% 男性
  • 绿色盲(deuteranopia):约 6% 男性(最常见)
  • 蓝色盲(tritanopia):极罕见

⭐ 关键代码

⭐ 关键点 1:RGB → HSL 手写

function rgbToHsl(r, g, b) {
  r /= 255; g /= 255; b /= 255;
  const max = Math.max(r, g, b), min = Math.min(r, g, b);
  let h, s, l = (max + min) / 2;
  if (max === min) { h = s = 0; }
  else {
    const d = max - min;
    s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
    switch (max) {
      case r: h = (g - b) / d + (g < b ? 6 : 0); break;
      case g: h = (b - r) / d + 2; break;
      case b: h = (r - g) / d + 4; break;
    }
    h /= 6;
  }
  return [h * 360, s * 100, l * 100];
}

⭐ 自己写 RGB↔HSL 是为了理解每一步的物理含义——"最大通道决定色相、相减决定饱和度"——比用 d3 库更能理解色相为什么在 0-360° 循环。

⭐ 关键点 2:CVD 模拟矩阵

const CVD_MATRICES = {
  deuteranopia: [
    [0.625, 0.375, 0.000],
    [0.700, 0.300, 0.000],
    [0.000, 0.300, 0.700]
  ],
  protanopia:   [[0.567, 0.433, 0.000], [0.558, 0.442, 0.000], [0.000, 0.242, 0.758]],
  tritanopia:   [[0.950, 0.050, 0.000], [0.000, 0.433, 0.567], [0.000, 0.475, 0.525]]
};

function simulateCVD([r, g, b], type) {
  const m = CVD_MATRICES[type];
  return [
    r * m[0][0] + g * m[0][1] + b * m[0][2],
    r * m[1][0] + g * m[1][1] + b * m[1][2],
    r * m[2][0] + g * m[2][1] + b * m[2][2]
  ].map(v => Math.round(Math.max(0, Math.min(255, v))));
}

⭐ 关键点 3:d3 内置多空间

const c = d3.color('rgb(100, 50, 200)');
c.rgb();   // {r, g, b}
c.hsl();   // {h, s, l}
c.lab();   // {l, a, b}  ← 感知均匀
c.hcl();   // {h, c, l}  ← Lab 极坐标版
加载中…

3.2 colormap 三类选择

顺序型 / 发散型 / 定性型。选错类型 = 误导读者。

三类 colormap 对应不同数据语义

  • 顺序型(Sequential):数据有自然顺序(人口、温度)。单调亮度 + 单调色相。例子:Viridis / Plasma / Cividis
  • 发散型(Diverging):数据有意义的"中点"(如 0、正负偏差、白人中位数)。两端异色,中间白/浅。例子:RdBu / BrBG / PiYG
  • 定性型(Qualitative):数据无序(如产品类别)。色相区分、亮度接近。例子:Tableau10 / Set1 / Set3

典型错误

用发散型 RdBu 画"50 州谋杀率"(有序数据)——中段接近白色,看上去"谋杀率 = 100 附近"的州是"中点"——但 100 没有任何语义,纯属误导。

CVD 友好检查清单

  • Viridis / Cividis:✅ 优(Nature Methods 推荐)
  • RdBu / BrBG:△ 中等(红绿对色盲不友好)
  • Tableau10(定性):△ 与色相相关
  • Jet(彩虹谱):⚠ 禁用

⭐ 关键代码

⭐ 关键点 1:d3 三类 scale 速记

// 顺序型:[0, 1] → 颜色
d3.scaleSequential(d3.interpolateViridis);

// 发散型:[0, 1] → 颜色(带中点反转)
d3.interpolateRdBu;          // t=0 蓝,t=1 红,t=0.5 白
d3.interpolateBrBG;

// 定性型:离散类别 → 颜色
d3.scaleOrdinal(d3.schemeTableau10);

⭐ 选错类型 = "撒谎"。比如用 Viridis 表示正负偏差,用户会以为"中点 = 白色"——但 Viridis 的中点是深紫色。

⭐ 关键点 2:决策表

function pickColormap(data) {
  if (data.hasMeaningfulMidpoint) return 'diverging';
  if (data.isOrdered)              return 'sequential';
  return 'categorical';
}
加载中…

3.3 CIE Lab 色域图

在感知均匀的颜色空间里,色域长什么样?

为什么需要 Lab 色域图?

RGB 空间是立方体,但你不知道"哪些颜色真的存在、哪些会失真"。CIE Lab 是感知均匀空间,距离 ≈ 人眼感知差,是设计 colormap 和检查色域的标准。

三个色域(固定 L* 截面)

  • sRGB:屏幕通用标准(Web/PPT/一般科研图)
  • Adobe RGB:摄影/印刷,绿域更广
  • Rec.2020:4K 电视/HDR,色域最大

下方演示统一使用 D65 参考白。边界随当前 L* 重新计算,因此不应假定三个色域在每一个 Lab 截面上都严格嵌套。

超出 sRGB 的颜色(如高饱和度的纯绿)在普通屏幕上显示不出来,会被 clamp。

实战用法

  • 从 d3.lab(L, a, b).rgb() 转换时,超出 sRGB 的颜色会自动 clamp
  • 设计 colormap 时用 d3.interpolateLab(在 Lab 空间插值),比 d3.interpolateRgb 平滑得多
  • 高饱和度色仅在打印/4K 屏幕上"存在",Web 上看不一定准确

⭐ 关键代码

⭐ 关键点 1:Lab 颜色对象

// 直接构造 Lab 颜色
const c = d3.lab(50, 30, -20);    // L*=50, a*=30, b*=-20
console.log(c.rgb());              // → RGB {r, g, b}(clamp 到 sRGB)
console.log(c.formatHex());       // → "#xxxxxx"

// 从 RGB 反向构造
const c2 = d3.color('#ff0080');
console.log(c2.lab());             // → {l, a, b}

⭐ d3.lab() 返回的是 Lab 颜色对象,但显示时还是转回 RGB。Lab 是"思考空间",RGB 是"显示空间"。

⭐ 关键点 2:在 Lab 空间插值

// 在 Lab 空间插值(感知均匀)
const interp = d3.interpolateLab('#000000', '#ffffff');
interp(0.5);   // → 中间灰(Lab 空间线性插值,结果感知均匀)

// vs 在 RGB 空间插值(不均匀)
const interpRgb = d3.interpolateRgb('#000000', '#ffffff');
interpRgb(0.5);  // → 中间灰(RGB 空间插值,但在 Lab 看来"不是中间")
加载中…

3.4 色系搭配规则(5 大类)

单色 / 类比 / 互补 / 三等分 / 分裂互补——什么时候用哪个?

5 大色系规则

  • 单色 Monochromatic:同一色相不同亮度/饱和。安全、低风险。
  • 类比 Analogous:色轮相邻 ±30°。和谐但缺少对比度。
  • 互补 Complementary:色轮正对面 180°。对比度强但易刺眼。
  • 三等分 Triadic:色轮 120° 等分。3 种类别时最常用。
  • 分裂互补 Split:互补色相邻 150°。温和版互补。

选择原则

  • 严肃科研:单色(Nature 黑白经济型配色)
  • 层级 / 分组:类比
  • 突出重点:互补(实验组 vs 对照组)
  • 3 种类别:三等分
  • 一般情况:分裂互补(不刺眼又有对比)

⭐ 关键代码

⭐ 关键点 1:HSL 色相差

// 5 大色系的"色相差"参数
const HUE_OFFSETS = {
  monochromatic: 0,     // 同色相
  analogous:     30,    // ±30°
  complementary: 180,   // 180° 对面
  triadic:       120,   // 120° 三等分
  split:         150    // 150° 邻补
};

// 生成 5 色调色板
function palette(baseH, offset) {
  return [-2, -1, 0, 1, 2].map(i => {
    const h = (baseH + i * offset + 360) % 360;
    return d3.hsl(h, 0.6, 0.55).formatHex();
  });
}

⭐ d3.hsl(h, s, l) 直接构造 HSL 颜色对象。HSL 中 H 是 0-360° 循环,相加要 mod 360。

加载中…

3.5 科研经典配色

Wong / Paul Tol / Okabe-Ito / Tableau / Paired / Viridis——顶标都在用什么?

3 大黄金标准

  • Wong 2011:Nature Methods 8:441 推荐,8 色 CVD 友好,灰度打印友好
  • Paul Tol:SRON Tech Note,bright(白底)/ muted(深底)/ high-contrast(对比
  • Okabe-Ito 2002:8 色,CVD 友好,早期 Nature/Science 默认

商业 / 通用

  • Tableau 10:商业图表之王,10 色,红绿对色盲不友好
  • Paired (12):配对设计(处理前/后),12 色过载

顺序型 / 发散型

  • Viridis:matplotlib/d3 默认,感知均匀 + CVD 友好
  • Cividid:Viridis 的 CVD 强化版(更蓝/黄分明)
  • RdBu:发散型红蓝

实战原则

  • 类别数 ≤ 7,类别多时改用连续 colormap
  • 必须经过 CVD 模拟测试
  • 优先使用 CVD 友好 + 灰度友好的(顶标都用这套)

⭐ 关键代码

⭐ 关键点 1:经典配色硬编码数组

const CLASSIC_PALETTES = {
  'Wong 2011':       ['#000000', '#E69F00', '#56B4E9', '#009E73', '#F0E442', '#0072B2', '#D55E00', '#CC79A7'],
  'Paul Tol bright': ['#4477AA', '#EE6677', '#228833', '#CCBB44', '#66CCEE', '#AA3377', '#BBBBBB'],
  'Okabe-Ito':       ['#000000', '#E69F00', '#56B4E9', '#009E73', '#F0E442', '#0072B2', '#D55E00', '#CC79A7'],
  'Tableau 10':      ['#4E79A7', '#F28E2B', '#E15759', '#76B7B2', '#59A14F', '#EDC948', '#B07AA1', '#FF9DA7', '#9C755F', '#BAB0AC'],
  'Viridis (seq)':   d3.range(0, 1.01, 0.1).map(d3.interpolateViridis),
  'RdBu (div)':      d3.range(0, 1.01, 0.1).map(t => d3.interpolateRdBu(1 - t))
};

⭐ 直接用 hex 数组硬编码比用 d3 内置 scheme 更稳——因为 scheme 名字在不同 d3 版本会变。

加载中…

3.6 对错案例对比

4 张图,3 错 1 对。点击你认为"对"的那张。

4 个典型错误

  • ⚠ 彩虹谱 Jet:非感知均匀 + CVD 不友好 + 打印不友好。Nature 已禁用。
  • ⚠ 红绿对比:红绿色盲眼中几乎变成同色(最严重的可访问性问题)
  • ⚠ 颜色过多 12 色:超过 7 种时眼睛无法区分
  • ✅ Wong 8 色:CVD 友好 + 灰度打印友好 + 类别数 ≤ 8

教学要点

让学生亲手投票,能加深对"为什么是错的"的理解——光看理论不如下场选错一次来得深。

⭐ 关键代码

⭐ 关键点 1:构建可点击的 quiz 卡片

// 4 张 SVG 卡片,点哪张就高亮 + 显示原因
cases.forEach((c, i) => {
  const card = document.createElement('div');
  card.className = 'quiz-card';
  card.innerHTML = `${makeSampleBars(c.palette)}<div>${c.name}</div>`;
  card.addEventListener('click', () => {
    cards.forEach((cc, ii) => {
      cc.classList.remove('correct', 'wrong');
      if (ii === i) cc.classList.add(c.type);  // 'correct' or 'wrong'
    });
    showResult(c);
  });
  grid.appendChild(card);
});

⭐ quiz 模式比"展示正确"更适合教学——让学生先犯错再揭示答案。

加载中…

3.7 鲜艳 + 低饱和 哲学生成器

颜色数量 ≤ 7、鲜艳颜色 + 低饱和度 = 好看又耐看。算法自动生成 5-7 色搭配。

你的配色哲学(落地到 3 条规则)

  1. 颜色数量 ≤ 7:超过 7 种眼睛就分不清
  2. 鲜艳颜色(高 H 区分度):用 H 拉开类间距离(≥30°)
  3. 低饱和度(S 30-50%):每个色都别太"冲",耐看

经验公式

  • 主色 S = 70-80%(鲜明但不刺眼)
  • 配套色 S = 30-50%(低饱和,耐看)
  • 强调色用互补或类比,不要全饱和
  • 高亮用 L=85-90%(如背景、注释)

7 色生成规则

主色 + 2 类比(±30°)+ 1 类比低饱和 + 1 互补 + 1 互补低饱和 + 1 高亮 = 7 色,符合"≤ 7"原则。

⭐ 关键代码

⭐ 关键点 1:7 色生成规则

function genPalette(H, S, L) {
  return [
    hslHex((H - 30 + 360) % 360, S * 0.6, L * 1.1),  // 类比 1 低饱和
    hslHex((H - 30 + 360) % 360, S, L),              // 类比 1
    hslHex(H, S, L),                                 // 主色
    hslHex((H + 30) % 360, S, L),                    // 类比 2
    hslHex((H + 30) % 360, S * 0.6, L * 1.1),        // 类比 2 低饱和
    hslHex((H + 180) % 360, S * 0.8, L * 0.95),      // 互补
    hslHex(H, S * 0.2, L * 1.4)                      // 高亮(极低饱和)
  ];
}

⭐ (H + offset + 360) % 360 是 HSL 色相循环的安全写法——避免负数或 > 360。

加载中…

3.8 CVD 论文图测

你的图在色盲眼里还看得懂吗?拿真实顶标图做测试。

3 个"经典论文图"案例

  • 图 1:5 类别散点(Wong 配色)——Nature 风格
  • 图 2:顺序型热图(Viridis)——最常见的科研图
  • 图 3:红绿对比(错误案例)——常见但严重

CVD 测试流程

  1. 把图导出 PNG
  2. 用 Coblis 或 Color Oracle 模拟
  3. 看是否仍能区分(亮度对比 / 形状 / 标签)

实测结论

  • Wong / Okabe-Ito / Paul Tol:✅ 在 3 种 CVD 下都能区分
  • 红绿对比:⚠ 在绿盲眼中几乎同色(最严重)
  • Nature/Science/Cell 已逐步迁移到 CVD 友好配色

⭐ 关键代码

⭐ 关键点 1:把 SVG 中所有 fill 做 CVD 模拟

function cvdTransformSvg(svgString, type) {
  const doc = new DOMParser().parseFromString(svgString, 'image/svg+xml');
  doc.querySelectorAll('[fill]').forEach(el => {
    const c = d3.color(el.getAttribute('fill'));
    if (!c) return;
    const rgb = [Math.round(c.rgb().r), Math.round(c.rgb().g), Math.round(c.rgb().b)];
    const sim = simulateCVD(rgb, type);
    el.setAttribute('fill', `rgb(${sim.join(',')})`);
  });
  return new XMLSerializer().serializeToString(doc);
}

⭐ 用 DOMParser 解析 SVG 字符串,遍历所有 fill 属性应用 CVD 矩阵。比手写 SVG 字符串拼接安全。

加载中…