一、问题背景:普通D3色阶对色盲用户不友好

做数据可视化的人应该都有过这种经历:辛辛苦苦把一堆数据用D3画成图表,看着颜色渐变流畅、信息层级清晰,结果给色盲同事看的时候,对方一脸懵——说分不清图里的颜色深浅,根本对应不上数值大小。这不是同事找茬,是大部分人默认用的D3色阶,天生就没考虑色盲用户的视觉特点。

先简单说下最常见的色盲类型:红绿色盲,大概占男性人口的8%左右,他们对红色和绿色的感知和普通人不一样,很多渐变色阶在他们眼里会变成同一种颜色。比如D3自带的d3.interpolateRdYlBu(红-黄-蓝)色阶,普通人眼里是从红到黄再到蓝的清晰渐变,红绿色盲用户看的话,红和绿相关的部分会混在一起,根本分不出数值高低。

再讲下D3里的核心工具d3.scaleSequential,这个工具是专门用来做连续色阶的,比如把0-100的数值范围,对应到一段渐变颜色上。普通人用的时候直接调用自带的插值函数就行,但自带的插值函数很多都没做色盲适配,这就是问题的根源。

二、核心解决方案:自定义适配色盲的D3色阶

解决这个问题的核心思路很简单:不用D3自带的插值函数,自己写一个适配色盲的颜色渐变规则,再把这个规则传给d3.scaleSequential就行。

这里要用到一个专门的颜色工具:chroma.js,这个工具能帮我们生成适合色盲用户的颜色方案,还能模拟色盲看到的颜色效果,非常实用。

2.1 适配色盲的颜色方案选择

适合色盲用户的颜色方案有几个核心特点:

  1. 尽量不用红和绿作为主渐变色;
  2. 颜色的明度(亮度)要有明显差异,因为色盲用户对明度的感知和普通人差不多;
  3. 尽量使用蓝、紫、灰等对色盲友好的颜色作为主色调。

这里给大家推荐一个经过验证的适配方案:从深灰(明度低)到亮蓝(明度高)的渐变,这个方案在红绿色盲眼里也能清晰区分明度差异,不会出现颜色混淆的情况。

2.2 完整实现示例

先明确本次示例的技术栈:HTML + JavaScript(包含D3.js v7 + Chroma.js v2)。

完整代码如下,包含详细注释:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>适配色盲的D3色阶示例</title>
    <!-- 引入D3.js v7 -->
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <!-- 引入Chroma.js v2 -->
    <script src="https://unpkg.com/chroma-js@2.4.2/chroma.min.js"></script>
</head>
<body>
    <!-- 用来放色阶条的容器 -->
    <div id="color-scale-container" style="width: 800px; height: 50px; margin: 20px 0;"></div>
    <!-- 用来放模拟色盲看到的色阶条的容器 -->
    <div id="color-blind-simulation-container" style="width: 800px; height: 50px; margin: 20px 0;"></div>

    <script>
        // -------------------------- 1. 定义适配色盲的颜色插值函数 --------------------------
        // 自定义插值函数:输入0-1的数值(0对应最低值,1对应最高值),返回对应的颜色
        function colorBlindFriendlyInterpolate(t) {
            // 起点颜色:深灰(明度低,对应数值小)
            const startColor = '#2c3e50';
            // 终点颜色:亮蓝(明度高,对应数值大)
            const endColor = '#3498db';
            // 使用Chroma.js生成渐变颜色,t是渐变的比例(0到1)
            return chroma.interpolate(startColor, endColor)(t).hex();
        }

        // -------------------------- 2. 创建适配色盲的D3连续色阶 --------------------------
        // 定义数值范围:假设我们的数据是0到100
        const valueRange = [0, 100];
        // 创建D3连续色阶,把自定义的插值函数传进去
        const colorScale = d3.scaleSequential(colorBlindFriendlyInterpolate)
            .domain(valueRange); // 绑定数值范围

        // -------------------------- 3. 绘制普通用户看到的色阶条 --------------------------
        // 选择色阶容器,设置SVG的宽度和高度
        const svg = d3.select('#color-scale-container')
            .append('svg')
            .attr('width', 800)
            .attr('height', 50);
        // 生成100个渐变的矩形,每个矩形对应一个数值
        const numRects = 100;
        const rectWidth = 800 / numRects;
        for (let i = 0; i < numRects; i++) {
            // 计算当前矩形对应的数值(0到100)
            const value = (i / numRects) * 100;
            // 给矩形填充对应的颜色
            svg.append('rect')
                .attr('x', i * rectWidth)
                .attr('y', 0)
                .attr('width', rectWidth)
                .attr('height', 50)
                .attr('fill', colorScale(value));
        }

        // -------------------------- 4. 模拟红绿色盲看到的色阶条 --------------------------
        const svgBlind = d3.select('#color-blind-simulation-container')
            .append('svg')
            .attr('width', 800)
            .attr('height', 50);
        for (let i = 0; i < numRects; i++) {
            const value = (i / numRects) * 100;
            const originalColor = colorScale(value);
            // 用Chroma.js模拟红绿色盲看到的颜色(protanopia是红绿色盲的一种)
            const blindColor = chroma(originalColor).protanopia().hex();
            svgBlind.append('rect')
                .attr('x', i * rectWidth)
                .attr('y', 0)
                .attr('width', rectWidth)
                .attr('height', 50)
                .attr('fill', blindColor);
        }
    </script>
</body>
</html>

把这段代码保存成HTML文件,在浏览器打开就能看到效果:上面的色阶条是普通人看到的深灰到亮蓝的渐变,下面的是模拟红绿色盲看到的效果,两个色阶条的明度差异都很明显,能清晰区分数值大小。

三、技术细节:自定义色阶的原理和扩展

很多人可能会问:d3.scaleSequential的插值函数到底是什么?为什么自定义这个函数就能适配色盲?

其实原理很简单:d3.scaleSequential的核心作用是把一个连续的数值范围(比如0-100),映射到一个0-1的渐变比例上(比如数值0对应比例0,数值100对应比例1,数值50对应比例0.5)。然后它会把这个0-1的比例传给插值函数,插值函数根据这个比例返回对应的颜色。

所以只要我们写的插值函数返回的颜色是适配色盲的,整个色阶就适配色盲了。

3.1 扩展:多色适配色阶

如果我们的图表需要区分更多的数值层级,比如分成“低、中、高”三个等级,也可以自定义多色的适配色阶。比如用深灰(低)→浅灰(中)→亮蓝(高)的渐变,代码修改如下:

// 自定义多色插值函数
function multiColorBlindFriendlyInterpolate(t) {
    // 定义多个颜色节点,每个节点对应一个比例和颜色
    const colorNodes = [
        { t: 0, color: '#2c3e50' }, // 比例0,深灰
        { t: 0.5, color: '#bdc3c7' }, // 比例0.5,浅灰
        { t: 1, color: '#3498db' } // 比例1,亮蓝
    ];
    // 找到当前比例t所在的两个节点
    let startNode, endNode;
    for (let i = 0; i < colorNodes.length - 1; i++) {
        if (t >= colorNodes[i].t && t <= colorNodes[i + 1].t) {
            startNode = colorNodes[i];
            endNode = colorNodes[i + 1];
            break;
        }
    }
    // 计算当前比例在两个节点之间的局部比例
    const localT = (t - startNode.t) / (endNode.t - startNode.t);
    // 生成渐变颜色
    return chroma.interpolate(startNode.color, endNode.color)(localT).hex();
}

这个函数的作用是:当比例t在0到0.5之间时,颜色从深灰渐变到浅灰;当t在0.5到1之间时,颜色从浅灰渐变到亮蓝。这样的色阶能区分更多的层级,同时依然适配色盲用户。

3.2 扩展:适配其他类型的色盲

除了红绿色盲,还有蓝黄色盲、全色盲等类型,我们可以根据需要修改插值函数里的颜色,适配不同的色盲类型。比如蓝黄色盲对蓝色和黄色的感知有问题,我们可以把主色调改成红和灰的渐变,同样能达到适配的效果。

四、应用场景、优缺点和注意事项

4.1 应用场景

这种适配色盲的自定义色阶,适合所有需要展示连续数据的D3图表,比如:

  1. 热力图:用来展示不同区域的数值大小;
  2. 折线图:用来展示数据随时间的变化趋势;
  3. 散点图:用来展示不同点的数值大小;
  4. 地图:用来展示不同地区的统计数据。

特别是面向大众的公共数据可视化、面向企业内部的数据分析平台,一定要做适配,不然会有大量用户看不懂图表。

4.2 技术优缺点

优点:

  1. 适配性强:可以根据不同的色盲类型、不同的图表需求,自定义颜色方案;
  2. 兼容性好:只需要修改d3.scaleSequential的插值函数,不需要修改D3的其他逻辑,原来的图表代码基本不用动;
  3. 可视化效果好:自定义的色阶可以保持清晰的明度差异,同时也能符合普通人的视觉习惯;
  4. 可验证:可以用Chroma.js模拟色盲看到的颜色效果,提前验证适配效果。

缺点:

  1. 有一定的开发成本:需要了解色盲的视觉特点,还要会用Chroma.js等工具;
  2. 颜色选择范围有限:适配色盲的颜色方案不能用红、绿、黄等容易混淆的颜色,选择范围比普通色阶小;
  3. 可能需要调整原有设计:如果原来的图表用了红绿色阶,改成适配色阶后,可能需要调整整体的视觉风格。

4.3 注意事项

  1. 尽量优先保证明度差异:色盲用户对明度的感知最稳定,所以不管选什么颜色方案,一定要保证颜色的明度有明显差异;
  2. 不要只用颜色区分信息:除了颜色,还可以用形状、大小、标签等方式区分信息,这样即使是全色盲用户也能看懂图表;
  3. 测试不同的色盲类型:如果面向的用户群体比较广,一定要测试红绿色盲、蓝黄色盲等多种类型的适配效果;
  4. 不要过度依赖工具:Chroma.js的模拟效果是近似的,最好能找真正的色盲用户测试,保证实际的适配效果。

五、文章总结

做数据可视化的核心是传递信息,要是有一部分用户因为颜色问题看不懂图表,那这个可视化就失去了意义。D3自带的色阶虽然方便,但大部分都没考虑色盲用户的需求,所以我们需要自己动手,基于d3.scaleSequential自定义适配色盲的色阶。

自定义的方法其实很简单:只要写一个适配色盲的插值函数,传给d3.scaleSequential就行。配合Chroma.js工具,我们可以轻松生成适配的颜色方案,还能模拟色盲看到的效果,提前验证适配情况。

不管是做什么类型的图表,适配色盲用户都是很重要的事情,不仅能提升产品的可用性,也能体现对用户的尊重。希望大家以后做D3可视化的时候,都能多考虑一下色盲用户的需求,做出更友好的图表。