D3.js 核心概念——比例尺(一)

2,249 阅读11分钟

原文(持续更新):datavis-note.benbinbin.com/article/d3/…


系列文章可以查看《数据可视化》专栏


参考:

本文主要介绍 Scales 模块

Jacques Bertin 在《Semiology of Graphics》中描述了图形符号的不同属性(例如位置、尺寸、颜色等)在展示数据时的适用性。而将两者联系起来的关键是比例尺,它将数据(某个维度)按照一定的规则映射到图形符号的(某个特定)属性上,这样就可以使用具体可视的图形符号表示抽象的数据了。

比例尺实际是将一个类型的数据转换为另一个数据的规则方法,例如通过线性比例尺,将将一年的每月的收益转换为条形图在 Y 坐标轴的高度值。D3 在模块 d3-scale 中提供了多种类型的比例尺,适用于将不同类型的抽象数据转换为图形符号的不同属性:

💡 在 D3 说明文档中,将输入数据的范围称为定义域 domain,转换后输出数据的范围称为值域 range


以下比例尺适用于定义域为连续型

连续型比例尺 Continuous Scales

连续型比例尺 Continuous Scales 用于将连续型的定义域映射到连续型的值域

该类型的比例尺(以下的 continuous)的常用方法:

  • continuous.domain(domainArr) 设置或返回定义域。如果没有传递参数,则返回当前比例尺的定义域;如果传递数组,则将定义域范围设置为该数组。

    💡 一般传递的数组必须包含两个元素,而且元素的类型是数值。如果数组的元素有多个(元素依此是递增或递减的),则可以创建映射关系更复杂的比例尺。⚠️ 如果定义域 domain 数组长度 M (元素个数)和值域 range 数组长度 N(元素个数) 不同,则映射的关系按照 min(N, M) 构建,超出范围的无法进行转换

    // 创建一个线性比例尺
    const color = d3.scaleLinear()
        .domain([-1, 0, 1]) // 定义域是 -1 到 1,其中 -1 到 0 是一部分(负值),0 到 1 是另一部分(正值) 
        .range(["red", "white", "green"]); // 负值映射的值域是从红色渐变到白色的 rgb 值;正值映射的值域是从白色渐变到绿色的 rgb 值
    
    color(-0.5); // "rgb(255, 128, 128)"
    color(+0.5); // "rgb(128, 192, 128)"
    
  • continuous.range(rangeArr) 设置或返回值域。在传递数组设置值域时,数组的元素不必是数值,只要是插值器支持的类型就可以,例如颜色值。⚠️ 如果希望比例尺支持 continuous.invert(value) 则值域的类型需要是数值。

    💡 如果希望同时设置值域和插值方法为 d3.interpolateRound(通过四舍五入插入整数),可以使用 continuous.rangeRound(rangeArr),那么此时传递的数组的元素类型只能是数值

  • continuous(value) 向比例尺传递一个定义域 domain 的值,返回相应的值域 range 的值

  • continuous.invert(value) 向比例尺传递一个值域的 range 的值,反过来得到定义域 domain 的值。这对于交互很有用,例如根据鼠标在图表的位置,反向求出并显式对应的数据。⚠️ 该方法只支持值域为数值类型的比例尺,否则返回 NaN

  • continuous.clamp(clampState)continuous(value)continuous.invert(value) 入参值超出定义域(或值域)的范围时,比例尺的行为会根据 clamp 而定。

    • 如果开启 clamping 夹具功能 clampState=true,比例尺返回的值会限制在相应范围内
    • 如果不开启 clamping 夹具功能 clampState=false,比例尺会进行推断得出超出范围的值
    const x = d3.scaleLinear()
        .domain([10, 130])
        .range([0, 960]);
    
    // 默认不开启 clamping 夹具功能
    x(-10); // -160 超出值域范围
    x.invert(-160); // -10 超出定义域范围
    
    // 开启 clamping 夹具功能
    x.clamp(true);
    x(-10); // 0 结果被限制在值域中
    x.invert(-160); // 10 结果被限制在定义域中
    
  • continuous.unknown(value) 设置当比例尺接受的入参为 undefinedNaN 时,应该返回的值。这对于数据集中存在部分缺失时很有用(当然最好是在数据清洗中进行处理),可以将数据项映射到一个特定的可视化属性值

  • continuous.nice() 编辑定义域的范围,通过四舍五入使其两端的值更「整齐」nice,例如对于定义域的原本范围是 [0.201479…, 0.996679…] 可以扩展为 [0.2, 1.0]

  • continuous.interpolate(interpolate) 自定义值域的插值函数

线性比例尺 Linear Scales

线性比例尺 Linear Scales 值域中的值 yy 与定义域中的值 xx 通过表达式 y=mx+by=mx+b 联系起来,这种映射方式可以在视觉元素的变量中保留数据的原始差异比例

使用方法 d3.scaleLinear(domain, range) 构建一个线性比例尺,入参是可选的,如果忽略则定义域和值域范围默认是 [0, 1],也可以在之后通过 continuous.domain(value)continuous.range(value) 设置定义域和值域。

const x = d3.scaleLinear([10, 130], [0, 960]);
const color = d3.scaleLinear([10, 100], ["brown", "steelblue"]);

<!-- 等价的方法 -->

const x = d3.scaleLinear()
    .domain([10, 130])
    .range([0, 960]);

x(20); // 80
x(50); // 320

const color = d3.scaleLinear()
    .domain([10, 100])
    .range(["brown", "steelblue"]);

color(20); // "#9a3439"
color(50); // "#7b5167"

💡 恒等比例尺 Identity Scales 是线性比例尺的一种特例,其定义域和值域相同,使用方法 d3.scaleIdentity(range) 构建

幂比例尺 Power Scales

幂比例尺 Power Scales 会对定义域的值 xx 进行幂运算,再与值域中的值 yy 联系起来 y=mxk+by=mx^{k}+b,其中 kk 是幂

使用方法 const pow = d3.scalePow(domain, range) 构建一个幂比例尺,默认的幂为 11(此时是线性比例尺)

使用比例尺方法 pow.exponent(k) 用来设置幂

💡 幂比例尺 pow(value) 可以接受负值的输入,此时入参的值和转换后得到的值都会乘以 1-1

💡 D3 还提供了一个方法 d3.scaleSqrt(domain, range) 方便地生成幂为 k=0.5k=0.5 的幂比例尺,等价于以下生成的幂比例尺

d3.scalePow()
  .exponent(0.5)

对数比例尺 Log Scales

对数比例尺 Log Scales 会对定义域的值 xx 进行对数运算,再与值域中的值 yy 联系起来 y=mlog(x)+by=mlog(x)+b

⚠️ 由于对数的限制 log(0)=log(0)=-\infty,对数比例尺的定义限范围是正值。如果需要支持负值,则需要对该比例尺进行封装,显式地对入参值和输出值都乘以 1-1 进行预转换

使用方法 const log = d3.scaleLog(domain, range) 构建一个对数比例尺,默认以 1010 作为底数

使用比例尺方法 log.base(N) 用来设置底数

💡 类似地,D3 还提供双对称的对数比例尺 Symlog Scales d3.scaleSymlog(domain, range)

径向比例尺 Radial Scales

径向比例尺 Radial Scales 是线性比例尺的一种变体,它将定义域的值与值域的值的平方构成线性关系,例如在径向条形图将数据映射为半径,而页面展示的图形元素则是面积

使用方法 d3.scaleRadial(domain, range) 构建一个径向比例尺

时间比例尺 Time Scales

时间比例尺 Time Scales 是线性比例尺的一种变体,它以时间对象 Date 作为定义域

使用方法 d3.scaleTime(domain, range) 构建一个时间比例尺,如果省略 domain 则默认定义域为 [2000-01-01, 2000-01-02]

const x = d3.scaleTime()
    .domain([new Date(2000, 0, 1), new Date(2000, 0, 2)])
    .range([0, 960]);

x(new Date(2000, 0, 1,  5)); // 200
x(new Date(2000, 0, 1, 16)); // 640
x.invert(200); // Sat Jan 01 2000 05:00:00 GMT-0800 (PST)
x.invert(640); // Sat Jan 01 2000 16:00:00 GMT-0800 (PST)

顺序比例尺 Sequential Scales

它和连续型比例尺类似,也是将连续型的定义域映射到连续型的值域,但该比例尺的值域一般是指定一个插值器

使用方法 const sequential = d3.scaleSequential(domain, interpolator) 构建一个顺序比例尺,如果值域的插值器省略,则默认使用恒等函数 identity function

const rainbow = d3.scaleSequential(d3.interpolateRainbow);

💡 如果值域/插值器是两个元素构成的数组,表示插值的范围,D3 会调用方法 d3.interpolate() 将它转换为一个插值器

该比例尺(以下的 sequential)除了有连续型比例尺的常用方法以外,还有一些不同的方法:

  • sequential.interpolator(interpolator) 设置比例尺的插值器
  • sequential.range(rangeArr) 设置插值的范围,D3 会将它转换为一个插值器

💡 和连续型比例尺类似,顺序比例尺有一些衍生的比例尺,可以先对定义域的值进行幂、对数等运算,进行转换后再传递给插值器处理

  • d3.scaleSequentialLog(domain, interpolator)d3.scaleSequentialSqrt(domain, interpolator)
  • d3.scaleSequentialPow(domain, interpolator)d3.scaleSequentialSymlog(domain, interpolator)
  • d3.scaleSequentialQuantile(domain, interpolator) 和分位数型比例尺类似

发散比例尺 Diverging Scales

它和连续型比例尺类似,也是将连续型的定义域映射到连续型的值域,但该比例尺的定义域一般是由三个元素组成的数组,值域一般是指定一个插值器

使用方法 const diverging= d3.scaleDiverging(domain, interpolator) 构建一个发散比例尺,如果没有设置定义域,默认值为 [0, 0.5, 1];如果没有设定插值器,则默认使用恒等函数 identity function

const spectral = d3.scaleDiverging(d3.interpolateSpectral);

分层比例尺 Quantize Scale

分层比例尺 Quantize Scale 用于将连续型的定义域映射到离散型值域,一般通过四舍五入等修约的方法将数据进行分层转换映射,以便将数据进行归类区分。

定义域的范围会根据离散型值域中可取值的数量划分为等距的片段,即每一个值域中离散值 yy,都可以代表一段定义域范围 y=m round(x)+by=m\space round(x)+b,例如等值域图/分级统计地图

使用方法 d3.scaleQuantize(domain, range) 构建一个分层比例尺,如果省略了定义域或值域,则它默认范围是 [1, 0],其作用就等价于 Math.round

const color = d3.scaleQuantize()
    .domain([0, 1])
    .range(["brown", "steelblue"]);

color(0.49); // "brown"
color(0.51); // "steelblue"

该类型的比例尺(以下的 quantize)的常用方法:

  • quantize.domain(domainArr) 设置定义域,数组由两个元素组成一个范围,而且元素的类型是数值,而且按升序排列

  • quantize.range(rangeArr) 设置值域,数组包含一些列离散的值

  • quantize(value) 向比例尺传递一个定义域 domain 的值,返回相应的值域 range 的值

  • quantize.invertExtent(value) 向比例尺传递一个值域的 range 的值,反过来得到定义域 domain 对应的片段

    const width = d3.scaleQuantize()
        .domain([10, 100])
        .range([1, 2, 4]);
    
    width.invertExtent(2); // [40, 70]
    
  • quantize.nice()

分位数比例尺 Quantile Scales

分位数型比例尺 Quantile Scales 将输入的数据作为总体(一堆通过采样获取的离散值),这样就可以接受在该总体范围中的任意值输入(即定义域是连续型),可以计算出它在总体中的分位数,然后基于分位数再找出在值域中一些列离散值中的那个对应值。

使用方法 d3.scaleQuantile(domain, range) 构建一个分位数比例尺

该类型的比例尺(以下的 quantile)的常用方法:

  • quantile.domain(domainArr) 设置定义域,数组由一堆通过采样获取的离散值,而且元素的类型是数值,然后 D3 会对数组进行拷贝并对元素进行排序,作为总体用于计算分位数

  • quantile.range(rangeArr) 设置值域,数组包含一些列离散的值

  • quantile(value) 向比例尺传递一个定义域 domain 的值,返回相应的值域 range 的值

  • quantile.invertExtent(value) 向比例尺传递一个值域的 range 的值,反过来得到定义域 domain 对应的片段

  • quantile.quantiles() 根据值域 range 的离散值数量 n,对分位数进行分段,再根据各段的分位数可以计算出对应的定义域的 domain 的值,该方法就是返回 n-1 个阈值构成的数组。

阈值比例尺 Threshold Scales

阈值比例尺 Threshold Scales 和分层型比例尺 Quantize Scale 类似,不过映射规则更自由,定义域数组中各元素是阈值,可以更灵活地对定义域进行任意的划分,然后 D3 将各段定义域分别映射到值域各个离散值

使用方法 d3.scaleThreshold(domain, range) 构建一个阈值比例尺

const color = d3.scaleThreshold()
    .domain([0, 1])
    .range(["red", "white", "green"]);

color(-1);   // "red"
color(0);    // "white"
color(0.5);  // "white"
color(1);    // "green"
color(1000); // "green"

该类型的比例尺(以下的 threshold)的常用方法:

  • threshold.domain(domainArr) 设置定义域,数组由一系列阈值组成,然后元素按升序排列
  • threshold.range(rangeArr) 设置值域,数组包含一些列离散的值

⚠️ 如果值域离散值数量是 N+1,则定义域数组中的阈值数量需要是 N。如果阈值数量少于期望的值,则相应的值域离散值会被忽略;如果阈值的数量多于期望的值,则在调用阈值比例尺时可能返回 undefined,因为在值域没有相应的离散值与该段定义域相对应。

  • threshold(value) 向比例尺传递一个定义域 domain 的值,返回相应的值域 range 的值

  • threshold.invertExtent(value) 向比例尺传递一个值域的 range 的值,反过来得到定义域 domain 对应的片段

    const color = d3.scaleThreshold()
        .domain([0, 1])
        .range(["red", "white", "green"]);
    
    color.invertExtent("red"); // [undefined, 0]
    color.invertExtent("white"); // [0, 1]
    color.invertExtent("green"); // [1, undefined]