比例尺 — 数据世界到像素世界的翻译器¶
这一章讲什么: 数据可视化本质上是按比例表示数据:把任意数值映射到位置、长度、颜色。 d3-scale 模块里的每个函数都是一台翻译器,本章把全部翻译器分类讲透: 连续的(线性、幂、对数、时间)、插值的(输出颜色)、离散的(切桶、类别映射)。 原书给了一套可交互的对比页(
Compare/文件夹),同一批数据换不同比例尺渲染,差异一目了然。
1. 顶层全景:一张选型地图¶
全部比例尺可沿着三个问题定位:
输入是连续的还是离散的?
├─ 连续输入
│ ├─ 输出也要连续(位置/长度)→ 连续比例尺
│ │ ├─ 比例关系重要 → scaleLinear(默认选择)
│ │ ├─ 数量级跨度大 → scaleLog(不能有 0)
│ │ ├─ 要有 0 又要对数感 → scaleSymlog
│ │ ├─ 要强调小数值差异 → scalePow / scaleSqrt
│ │ └─ 输入是日期 → scaleTime / scaleUtc
│ └─ 输出是颜色 → 插值比例尺
│ ├─ 单向变化 → scaleSequential
│ └─ 正负两极 + 中点 → scaleDiverging
└─ 输入是类别
├─ 输出位置(柱状/散点)→ scaleBand / scalePoint
├─ 输出颜色等样式 → scaleOrdinal
└─ 想把连续值切成几档 → scaleQuantize / scaleQuantile / scaleThreshold
图说:先分输入,再分输出,最后按数据的数学性质挑非线性变体。
2. 连续比例尺:domain 进、range 出¶
所有连续比例尺共享一套心智模型:domain(定义域)= 数据的取值范围,range(值域)= 屏幕上的输出范围, 调用比例尺函数就是做一次带插值的映射1。
const scaleA = d3.scaleLinear().domain([0,1000]).range([100,500]);
scaleA(0) // 100 —— domain 下限 → range 下限
scaleA(250) // 200 —— 四分之一处
scaleA(1000) // 500
const scaleB = d3.scaleLinear().domain([0,1000]).range([500,100]); // range 反装
scaleB(250) // 400 —— 斜率变负:y 轴向上的手法(第 08 章)
图说:range 两端一换,函数就反向——不需要任何专门配置。
配置方法(连续比例尺通用,全部可链式)2:
| 方法 | 干什么 | 备注 |
|---|---|---|
domain(数组) / range(数组) |
设输入/输出范围 | 可传多段(见下) |
rangeRound(数组) |
range 取整 | 防抗锯齿模糊 |
invert(值) |
反向翻译:像素 → 数据 | 交互取数必备 |
nice() |
把 domain 撑成整数端点 | 轴刻度更整齐 |
clamp(bool) |
超界输入钳到端点 | 默认 false |
interpolate(工厂) |
换插值器(如 HSL 空间) | 颜色专用 |
unknown(值) |
undefined/NaN 输入的输出 | 默认 undefined |
exponent/base/constant |
幂/对数/对称对数的参数 | 各归各的尺 |
2.1 线性比例尺与分段¶
scaleLinear 就是 f(x) = ax + b,保比例差。domain 和 range 都可以分段:
每对相邻端点之间独立插值3:
const scale = d3.scaleLinear().domain([-10,0,10]).range([0,900,1000]);
图说:负半段映射到 0–900(宽),正半段挤进 900–1000(窄)——
想把某段数据放大看的标准手法,斜率每段不同。
clamp 与 invert 是两个高频工具,原书给了具体的数4:
scale(64) // 3700 —— 超界就外推,不会报错
scale.clamp(true)
scale(64) // 1000 —— 钳到端点
scale(-10) // 0
scale.invert(300) // -4 —— 用户点在 300 像素处 → 数据值 -4
图说:invert 是交互图的地基——点击/悬停的坐标全靠它译回数据值。
2.2 非线性连续尺¶
| 比例尺 | 变换 | 什么时候用 |
|---|---|---|
scalePow().exponent(k) |
f = axᵏ+b | 想弯曲线性关系;平方强调大值,开方强调小值 |
scaleSqrt() |
= scalePow().exponent(0.5) | 面积↔半径的换算(面积比 = 半径比²) |
scaleLog().base(k) |
f = a·log(x)+b | 数量级跨度大;domain 不能有 0 |
scaleSymlog().constant(k) |
中间线性、两侧对称对数 | 要有 0、有负数、又要压数量级 |
scaleTime / scaleUtc |
线性,domain 是 Date | 日期;Utc 版不受本地时区影响 |
原书的主线演示:同一列 −10…10 的数,画在 1000 像素轴上——
scalePow().exponent(2): 平方后再映射 → 大值被拉开,点列呈 U 形曲线
scaleSqrt(): 开方后再映射 → 小值被拉开
scaleLog().domain([1,20]): 1~10 占去左半边,11~100 挤在右半边——
每个等距刻度代表 ×10(默认 base 10)
base(2): 等距刻度代表 ×2
scaleSymlog(): (-1,1) 之间是直线,往外是对数曲线,零附近平滑过渡
图说:非线性尺不扭曲数据,只扭曲「距离」的分配——
把像素预算花在数据密集或你关心的区间。
对数的另一条死线:domain 严格正或严格负,不能含 0(log(0) 无定义)。
既要对数感又要 0 和负数,才轮到 symlog——它由三段函数拼成:中间 (−k, k) 线性,
两侧各自对称对数,constant(k) 调线性区宽度(默认 1)5。
2.3 时间比例尺¶
scaleTime 是 domain 为 Date 对象的线性尺——内部按毫秒插值,刻度自动生成日历区间
(跨年就出年份,单日就出小时)6:
const scale = d3.scaleTime()
.domain([new Date(2017,9,1), new Date(2018,9,1)])
.range([0,1000]);
// 三个日期点 [10/1, 11/26, 次年 3/15] 映射到 ~117, ~233, ~550 像素附近
// 单日 domain(0:00–23:59)→ 刻度自动变「日 + 小时」
// scaleUtc 同数据、同位置,刻度按 UTC 摆——时差让轴整体平移
3. 插值比例尺:输出是颜色¶
顺序比例尺(scaleSequential) = 连续尺 + 插值器:把 domain 线性压到 [0,1], 交给插值器产出输出(通常是颜色)。默认 range 就是 [0,1],你只配 domain7:
const scale = d3.scaleSequential(d3.interpolateRainbow).domain([0,600]);
d3.range(0,601,5).forEach(d => /* 每 5 单位一根 4px 色条,填 scale(d) */)
// → 一条 600 像素的彩虹
图说:rainbow 插值器吃 t∈[0,1] 出颜色;scale 负责把 0–600 归一化。
非线性变体(scaleSequentialPow(d3.interpolateRainbow).exponent(3))把颜色预算重分配:
指数 3 让前半段几乎跑完整个光谱,蓝色独吞后半——数据分布不均时让颜色用得更公平8。
发散比例尺(scaleDiverging) = domain 三段的顺序尺:[负极, 中点, 正极],
中点映射到插值器的 0.5。配 d3-scale-chromatic 的发散配色(如 interpolateBrBG 棕-绿)9:
const scale = d3.scaleDiverging(d3.interpolateBrBG).domain([-300, 0, 300]);
// 负值 → 棕色系,0 → 中性色,正值 → 绿色系
// 中点不必是 0:温度距平常取多年均值,domain 变成 [-3, 0.8, 3] 之类
发散 vs 顺序的选择依据原书一句话:要强调正负两极,用发散;单向渐变,用顺序10。
4. 离散比例尺:切桶与对位¶
离散比例尺分两类:分区(连续输入切成几桶)和序数(离散输入对位到输出)。
4.1 三个切桶尺:quantize / quantile / threshold¶
主线例子:同一批数,三种桶法,四个颜色11:
data = [1,9,14,17,20, 29,31,45,49, 51,56,62, 80,89,99]
① scaleQuantize(等宽切)
.domain([0,100]).range(['blue','red','green','orange'])
scale.thresholds() // [25, 50, 75]
切法:0–25 蓝,25–50 红,50–75 绿,≥75 橙 —— 每段等宽,与数据分布无关
→ 1–20 蓝(5 个),29–49 红(4 个),51–62 绿(3 个),80–99 橙(3 个)
② scaleQuantile(等数切)
.domain(sample).range(同四色),sample = [1,3,4, 8,9,20, 30,36,41, 45,48,96]
scale.quantiles() // [7, 25, 42]
切法:按样本的分位数定界——每段约 3 个点,桶宽不等
→ 同样的 data,各档点数被强行拉平
③ scaleThreshold(手切)
.domain([10,20,80]).range(同四色)
切法:<10 蓝,10–20 红,20–80 绿,≥80 橙 —— 阈值自己定,业务口径说了算
图说:等宽(quantize)不看数据分布;等数(quantile)完全由样本分位数定;
阈值(threshold)把业务规则(如 HDI 等级 0.550/0.700/0.800)直接写进 domain。
三个尺都支持 invertExtent(输出值) 反查区间:scale.invertExtent("red") 返回 [25, 50]12。
原书 exercise 9.4–9.6 用同一批 HDI 数据把三种尺各练一遍——对比着学,差异记得最牢。
4.2 序数比例尺:类别对位¶
scaleOrdinal:domain 与 range 一一对应;domain 比 range 长时,range 循环13:
const colorScale = d3.scaleOrdinal()
.domain(d3.range(0,501,25)) // 21 个值
.range(['black','blue','red','green']); // 4 个颜色 → 循环 5 轮多
colorScale.unknown('gray') // 不在 domain 里的输入,不再自动加进 domain,而是给灰色
unknown() 是个行为开关:默认情况下序数尺遇到没见过的输入会把它加进 domain;
设了 unknown 之后变成「陌生值一律给这个后备值」14。
4.3 band 与 point:位置专用序数尺¶
柱状图和分类散点的位置,不用 scaleOrdinal 硬凑,有两个专职尺:
band:把 range 切成 N 条等宽带,每带对应一个类别。核心方法15:
const scale = d3.scaleBand().domain(d3.range(0,100,10)).range([0,800]);
scale(30) // 第 4 条带的起点 x
scale.bandwidth() // 带的宽度 → 直接当柱宽
scale.step() // 相邻带起点间距(bandwidth + paddingInner)
.padding(v) // 内、外边距一起设(占 step 的比例)
.paddingInner(v) // 带间空隙
.paddingOuter(v) // 两端空隙
.align(0~1) // 图在边距里的对齐(0.5 = 居中,默认)
图说:柱宽、柱间距、两端留白全由 band 尺统一发配——第 03 章柱状图的 yscale 就是它。
point:把 range 切成 N 个等距点(没有带宽),散点的分类 x 轴标配:
const scale = d3.scalePoint().domain(d3.range(0,100,10)).range([0,800]);
.attr("cx", scale) // 10 个点均匀落在 0–800
.padding(v) 以 step 为单位把点群往里收;.align() 调对齐;.step() 取点距
5. 在线教程:散点图(第一部分)¶
GDP 人均(国际元,对数跨度)× HDI 的散点:加载联合国 CSV → 定尺寸与边距 → 配比例尺(这里用对数 x 轴!)→ 画点 → 加轴和标签 → 回调比例尺到最优。 第二部分接在第 10 章后加颜色、悬浮提示与交互16。
6. 作者的判断与证据¶
| 判断 | 性质 | 依据 |
|---|---|---|
| 「数量级跨度大用对数,有 0 用 symlog」 | 选型法则 | log 不能含 0 的数学限制 + symlog 三段结构5 |
| 「发散尺强调两极,顺序尺单向渐变」 | 选型法则 | domain 三段结构与插值器 0.5 中点的对应关系10 |
| 「量化等宽、分位等数、阈值按业务」 | 三者分界的判据 | 同一份数据三桶法的对照演示11 |
7. 边界与局限¶
scaleRadial(半径平方尺)在函数表里一带而过,本章没有展开示例17。nice()、rangeRound只给了定义,组合使用的时机(如配合 axis)留给了散点教程。- 比例尺的性能(大数组逐点映射)与缓存没有讨论。
- 本章例图全是「数轴 + 圆点」的抽象形式,真实图表中的比例尺集成要看教程和第 11 章起。
判断(我们的,不是书里的): 这一章的函数表看着吓人(20+ 个生成器),但它们全是 同一个抽象的三个参数的排列组合:输入类型(连续/离散)× 变换(线性/幂/对数/对称对数)× 输出类型(连续/离散/插值器)。掌握了这个三维分解,记不住函数名也能推出来该用哪个。 如果错,会错在: 个别生成器不完全服从这个分解(如 scaleRadial 的平方输出、 scaleIdentity 的恒等特例),把它当完备分类法会漏掉边角——但对选型而言,这个分解够用了。
8. 可带走的¶
- 比例尺 = 函数:
scale(数据值)返回像素/颜色/类别;domain 与 range 是两次声明; - 连续尺三问:保比例?跨数量级?有零有负?——依次指向 linear → log → symlog;
- range 反装 = 函数反向(y 轴向上的标准手法);
clamp钳超界、invert反翻译(交互取数)、nice撑整数端点、unknown兜 NaN;- 日期用 scaleTime(本地)/scaleUtc(零时区),刻度自动按跨度换日历单位;
- 颜色输出走插值尺:单向 scaleSequential、两极带中点 scaleDiverging,配 d3-scale-chromatic 的插值器;
- 指数 >1 把颜色预算压向前半段——数据密集区颜色更分得开;
- 切桶三兄弟:quantize 等宽、quantile 等数(由样本分位数定界)、threshold 手切业务阈值;
invertExtent反查桶区间;thresholds()/quantiles()看切点;- 类别对位:ordinal 循环 range、band 出带宽(柱宽)、point 出等距点(散点 x);
- band/point 的 padding 都以 step 为单位,align 管整体对齐。
9. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 本章开头与手写 scale 对照 | Scales | text/16-ch09-chapter-9-scales.txt:23(搜「What are scales?」) · text/16-ch09-chapter-9-scales.txt:31(搜「createLinearScale」) |
| 连续尺函数表 | Scales | text/16-ch09-chapter-9-scales.txt:64(搜「Continuous scales」) · text/16-ch09-chapter-9-scales.txt:73(搜「scaleLinear」) |
| domain/range 与反装 | Scales | text/16-ch09-chapter-9-scales.txt:110(搜「scaleA = d3.scaleLinear()」) · text/16-ch09-chapter-9-scales.txt:115(搜「returns 200」) · text/16-ch09-chapter-9-scales.txt:120(搜「[500,100]」) |
| 配置方法表 | Scales | text/16-ch09-chapter-9-scales.txt:137(搜「domain(array)」) · text/16-ch09-chapter-9-scales.txt:146(搜「invert(value)」) · text/16-ch09-chapter-9-scales.txt:152(搜「clamp(boolean)」) |
| 多段线性尺 | Scales | text/16-ch09-chapter-9-scales.txt:216(搜「domain([-10,0,10])」) |
| clamp/invert 实跑 | Scales | text/16-ch09-chapter-9-scales.txt:228(搜「returns 3700」) · text/16-ch09-chapter-9-scales.txt:243(搜「invert(300)」) |
| 幂/根/对数/symlog | Scales | text/16-ch09-chapter-9-scales.txt:255(搜「exponent(2)」) · text/16-ch09-chapter-9-scales.txt:264(搜「scaleSqrt()」) · text/16-ch09-chapter-9-scales.txt:284(搜「scaleLog().domain([1,20]」) · text/16-ch09-chapter-9-scales.txt:294(搜「base(2)」) · text/16-ch09-chapter-9-scales.txt:315(搜「scaleSymlog」) · text/16-ch09-chapter-9-scales.txt:324(搜「constant(10)」) |
| 时间比例尺 | Scales | text/16-ch09-chapter-9-scales.txt:338(搜「new Date(2017,9,1)」) · text/16-ch09-chapter-9-scales.txt:361(搜「2018,9,15,6,30,0」) · text/16-ch09-chapter-9-scales.txt:374(搜「scaleUtc()」) |
| 顺序与发散插值尺 | Scales | text/16-ch09-chapter-9-scales.txt:417(搜「scaleSequential」) · text/16-ch09-chapter-9-scales.txt:440(搜「interpolateRainbow」) · text/16-ch09-chapter-9-scales.txt:456(搜「exponent(3)」) · text/16-ch09-chapter-9-scales.txt:470(搜「diverging scale」) · text/16-ch09-chapter-9-scales.txt:502(搜「interpolateBrBG」) |
| 离散尺总表与切桶三兄弟 | Scales | text/16-ch09-chapter-9-scales.txt:522(搜「Discrete scales」) · text/16-ch09-chapter-9-scales.txt:591(搜「[0,100]」) · text/16-ch09-chapter-9-scales.txt:599(搜「[ 25.00, 50.00, 75.00 ]」) · text/16-ch09-chapter-9-scales.txt:635(搜「sample = [1,3,4」) · text/16-ch09-chapter-9-scales.txt:642(搜「[ 7.00, 25.00, 42.00 ]」) · text/16-ch09-chapter-9-scales.txt:661(搜「thresholds = [10, 20, 80]」) · text/16-ch09-chapter-9-scales.txt:622(搜「invertExtent("red")」) |
| ordinal 与 unknown | Scales | text/16-ch09-chapter-9-scales.txt:686(搜「d3.range(0,501,25)」) · text/16-ch09-chapter-9-scales.txt:713(搜「unknown('gray')」) |
| band 与 point | Scales | text/16-ch09-chapter-9-scales.txt:727(搜「Returns the distance between」) · text/16-ch09-chapter-9-scales.txt:760(搜「scaleBand().domain(data)」) · text/16-ch09-chapter-9-scales.txt:799(搜「scalePoint().domain(data)」) |
| 散点教程第一部分 | Scales | text/16-ch09-chapter-9-scales.txt:828(搜「scatterplot」) |
| 章小结 | Scales | text/16-ch09-chapter-9-scales.txt:849(搜「Summary」) |
10. 练习题的三个选型场景(加厚:把选型地图用熟)¶
原书练习 9.1–9.3 是三道「选错比例尺就翻车」的对照题,正好把本章选型地图用一遍:
- 点挤成一团(练习 9.1):两幅散点图,一幅点挤在左下角(数量级跨度大), 一幅挤在一条线附近(乘法关系)。前者换 scaleLog(第 09 章 §2.2 的数量级判据), 后者换 scaleSqrt(平方关系配开方尺,曲线拉直)。判据:先看点的「挤法」—— 沿轴挤=换非线性,沿对角线挤=换幂次。
- 行星三围对比(练习 9.2):直径、体积、密度三个量纲放一张图,体积差了九个数量级—— 体积轴必须 log;密度在窄区间线性即可。一个坐标系里允许混用尺型,每根轴独立选型。
- 气温距平 1880–2024(练习 9.3):x 是年份(时间尺),y 是距平值(有负有正)—— 上色时 domain 取 [-3, 0.8, 3] 这类非零中点的三段域,把「正常年份」压进窄色带—— 这是第 10 章发散配色的前置课:中点选在数据分布的「平常处」,而不是默认的 0。
顺带两个本章没展开、出门会撞见的边角:
d3.scaleIdentity():恒等尺([0,1]→[0,1]),画「像素即数据」的参考图时用;scaleRadial():输出是输入的平方——半径编码面积时的专用尺(与 scaleSqrt 方向相反, 一个是「面积进半径出」,一个是「值进面积出」),用反了图的面积感就全错。
-
出处:「Scales」第 23–35 段(
text/16-ch09-chapter-9-scales.txt:23,搜「What are scales?」;:31,搜「createLinearScale」)。原书先手写 f(x)=ax+b 的生成器,再指出d3.scaleLinear()干同样的事。 ↩ -
出处:「Scales」第 130–171 段(
text/16-ch09-chapter-9-scales.txt:137,搜「domain(array)」;:146,搜「invert(value)」;:152,搜「clamp(boolean)」)。方法表照原书 Table 9.2 转写。 ↩ -
出处:「Scales」第 211–221 段(
text/16-ch09-chapter-9-scales.txt:216,搜「domain([-10,0,10])」;:217,搜「[0,900,1000]」)。 ↩ -
出处:「Scales」第 225–247 段(
text/16-ch09-chapter-9-scales.txt:228,搜「returns 3700」;:233,搜「returns 1000」;:243,搜「invert(300)」)。「点击坐标译回数据值」的交互场景见第 245–246 段。 ↩ -
出处:「Scales」第 280–328 段(
text/16-ch09-chapter-9-scales.txt:284,搜「scaleLog().domain([1,20]」;:308,搜「only support values greater than zero」;:309,搜「combination of three functions」;:324,搜「constant(10)」)。 ↩↩ -
出处:「Scales」第 332–384 段(
text/16-ch09-chapter-9-scales.txt:338,搜「new Date(2017,9,1)」;:357,搜「depending on the domain」;:361,搜「2018,9,15,6,30,0」;:374,搜「scaleUtc()」)。 ↩ -
出处:「Scales」第 411–451 段(
text/16-ch09-chapter-9-scales.txt:417,搜「scaleSequential」;:440,搜「interpolateRainbow」;:448,搜「default range is [0,1]」)。 ↩ -
出处:「Scales」第 453–463 段(
text/16-ch09-chapter-9-scales.txt:456,搜「exponent(3)」;:459,搜「increases the diversity of colors near the start」)。 ↩ -
出处:「Scales」第 469–514 段(
text/16-ch09-chapter-9-scales.txt:470,搜「diverging scale」;:502,搜「interpolateBrBG」;:508,搜「central value is the neutral value」)。 ↩ -
出处:「Scales」第 466–467、518–520 段(
text/16-ch09-chapter-9-scales.txt:466,搜「highlight extreme positive and negative values」;:518,搜「highlight their sequential aspects」)。 ↩↩ -
出处:「Scales」第 582–676 段(
text/16-ch09-chapter-9-scales.txt:591,搜「[0,100]」;:599,搜「[ 25.00, 50.00, 75.00 ]」;:604,搜「data = [1,9,14,17,20」;:635,搜「sample = [1,3,4」;:642,搜「[ 7.00, 25.00, 42.00 ]」;:661,搜「thresholds = [10, 20, 80]」)。 ↩↩ -
出处:「Scales」第 619–622 段(
text/16-ch09-chapter-9-scales.txt:622,搜「invertExtent("red")」)。 ↩ -
出处:「Scales」第 678–705 段(
text/16-ch09-chapter-9-scales.txt:682,搜「the range will repeat」;:686,搜「d3.range(0,501,25)」)。 ↩ -
出处:「Scales」第 710–716 段(
text/16-ch09-chapter-9-scales.txt:713,搜「unknown('gray')」)。 ↩ -
出处:「Scales」第 720–770 段(
text/16-ch09-chapter-9-scales.txt:727,搜「Returns the distance between」;:760,搜「scaleBand().domain(data)」;:768,搜「attr("x", scale)」)。 ↩ -
出处:「Scales」第 828–847 段(
text/16-ch09-chapter-9-scales.txt:830,搜「Gross Domestic Product」;:838,搜「6-log-scale」)。 ↩ -
出处:「Scales」第 82–84 段(
text/16-ch09-chapter-9-scales.txt:82,搜「scaleRadial」)。全章无 scaleRadial 的独立示例。 ↩