线图与面积图 — d3-shape 的形状生成器
线图与面积图 — d3-shape 的形状生成器
这一章讲什么: 从数据到一条线,中间隔着一串 path 命令字符。 d3-shape 的形状生成器(shape generators)替你把这串字符算出来:
d3.line()吃一组点、吐一条折线,d3.area()加一条基线闭合成面, 极坐标有lineRadial/areaRadial,想平滑有十几个 curve 算法,点标记有d3.symbol。 这一章的原书标题是「创建线图与面积图」,但它真正教的是生成器函数这个模式—— 下一章的饼图、堆叠图,和第 15 章的树,全是同一个模式的变体。
1. 顶层全景:八个形状生成器
d3-shape 共有八个生成器,本章覆盖六个(arc 和 link 留给第 12、15 章)1:
输入:数据数组 输出:SVG path 的 d 字符串(或 Canvas 命令,见第 12 章)
┌─────────────────────────────────────────────────────┐
│ d3.line() 二维点列 → 折线/曲线 │
│ d3.area() 二维点列 → 封闭面(topline+baseline)│
│ d3.lineRadial() (角度,半径)点列 → 极坐标线 │
│ d3.areaRadial() (角度,半径) → 极坐标环带 │
│ d3.symbol() → 单个符号路径(14 种预置形状) │
│ d3.arc() (角,半径) → 圆弧(第 12 章) │
│ d3.link() 两个点 → 曲线连边(第 15 章) │
└─────────────────────────────────────────────────────┘
图说:每个生成器 = 一个函数;先造函数、配访问器,再调用它把数据变成字符串。2. 核心原理
2.1 主线例子:五个点的一条线
本章的主线输入,原书反复用这五个坐标2:
const coords = [ [0,0], [100,200], [200,50], [300,150], [400,100] ];
① 造生成器 const line = d3.line();
② 算字符串 const pathData = line(coords);
// "M0,0L100,200L200,50L300,150L400,100"
③ 挂上页面 d3.select("svg").append("path").attr("d", pathData);
// CSS: path { stroke: red; fill: none; } ← fill 必须清掉,默认是黑色实底
图说:M = 移到起点,L = 直线连到。字符串里的每个数字都来自数据点。推荐写法是数据绑定版——这里有个值得停下脚步的语法点(原书专门加了 Important note):
d3.select("svg").append("path")
.datum(coords) // 整个数组作为一个数据项绑给 path
.attr("d", line); // ← 直接引用函数,等价于 .attr("d", c => line(c))
图说:datum() 与 data() 不同——它是「一对一」绑单个值(第 05 章);
attr 的回调收到绑定的数组,line 函数吃数组正好对口。为什么线图用 datum 而不用 join? 因为一条线是一个 <path>、吃一整个数组;
join 是「一个数据项一个元素」的批量模式,适合散点,不适合「全体点合成一个图形」的线。
多条线时(每条线一个数组)才轮到 join 出场——外层 join 绑「数组的数组」3。
2.2 配置线函数:访问器、defined、digits、curve
d3.line() 的配置方法(全部可链式、不带参数时是读取)4:
| 方法 | 默认 | 干什么 |
|---|---|---|
x(fn) / y(fn) | d => d[0] / d => d[1] | 取每个点的坐标;比例尺就放在这里 |
defined(fn) | 全 true | 返回 false 的点断开线(缺数据的洞) |
digits(n) | 3 | path 字符串的小数位数;0 = 整数坐标,文件更小 |
curve(工厂) | curveLinear | 连线算法(见 2.4) |
context(ctx) | null | 传 Canvas 上下文则出 Canvas 命令(第 12 章) |
对象数组与带比例尺的配置,是真实图表的两种常态5:
// 对象数组:配访问器
const line = d3.line().x(d => d.step).y(d => d.value);
// 比例尺:访问器里直接调 scale
const line = d3.line().x(d => scale.x(d[0])).y(d => scale.y(d[1]));
// 等价于把两个访问器当参数:d3.line(d => scale.x(d[0]), d => scale.y(d[1]))defined 的用途:缺测数据不断线、留缺口——
d3.line().defined(d => d && !isNaN(d[1]) && d[1] >= 0)
// y 值为 NaN 或负数的点跳过,线在那里断开而不是斜穿过去2.3 多序列线图:三层数据结构
多序列数据的形状是「数组的数组」,代码因此有三层上下文,原书调试图里把它们画成了塔6:
datasets = [
[ [0,·], [5,·], [10,·], [15,·], [20,·] ], ← 序列 1
[ [0,·], [5,·], [10,·], [15,·], [20,·] ], ← 序列 2
[ [0,·], [5,·], [10,·], [15,·], [20,·] ] ← 序列 3
]
x 轴共享 steps = [0,5,10,15,20]
外层 data(datasets) → 每个序列绑一个 <path>(class="dataset")
中层 line 函数被调 → 收到的 d 是一整个序列(坐标对数组)
内层 x()/y() 访问器 → 收到的 d 是单个坐标对 [x, y]
container.selectAll("path.dataset")
.data(datasets)
.join("path")
.attr("class", "dataset")
.attr("d", line) // line(一个序列) → path 字符串
.style("stroke", colorScale); // 每序列一色,序数比例尺
图说:访问器的上下文比 attr 回调低一层——这个「一层之差」是多序列图最容易错的地方。domain 用展平数据一次算完:d3.extent(datasets.flat(), d => d[0])——
flat() 把三层压成一层,extent 取总范围7。坐标轴照旧用第 08 章的 chart-utils:
utils.cartesianAxes().container(container).xScale(scale.x).yScale(scale.y)();调试法(path 生成器不画图时):把 line 函数单独喂一个序列,检查输出字符串里有没有
NaN——有,就是访问器或比例尺返回了非数;attr("d", d => console.log(d) || line(d))
可以看到每个元素到底收到的是什么8。
2.4 曲线函数:把折线变平滑
curve() 换的是「点与点之间怎么连」。同一组点,不同算法的性格9:
| 曲线 | 性格 | 调节参数 |
|---|---|---|
curveLinear | 默认,直线 | —— |
curveStep 系列 | 阶梯:水平再垂直(中点/前/后三种变奏) | —— |
curveBasis | B 样条,不过控制点,最光滑 | beta() 权重 |
curveCardinal | C 样条,过控制点 | tension() 0–1 |
curveCatmullRom | 过控制点的折中款 | alpha():0 更滑、1 更紧 |
curveNatural | 自然样条 | —— |
各类 …Closed | 首尾闭合变体 | —— |
curveMonotoneX/Y | 保单调,不出峰 | —— |
d3.line().curve(d3.curveBasis) // 一句换样条
d3.line().curve(d3.curveCatmullRomClosed.alpha(0.2)) // 参数版
图说:原书 Curves/3-curves-interactive.html 可以拖点、换算法、调参数——
选型别靠记,靠在那个页面里试。原书的选择建议藏在练习题里:地震次数、月度温度这类「数据本身不含中间值估计」的序列, 用 step 诚实;示意性的平滑趋势用样条,但样条会造出数据里没有的峰和谷——这是它的代价。
2.5 面积图:line + baseline
d3.area() 与 line 几乎一样,多一条基线(y0),闭合出一条带:
const coords = [ [0,0], [100,200], [200,50], [300,150], [400,100] ];
area(coords)
// M0,0L100,200L200,50L300,150L400,100 ← topline,和 line 完全一样
// L400,0L300,0L200,0L100,0L0,0Z ← baseline 折返(默认 y=0),闭合
图说:粗体部分就是 baseline——area 默认假设基线是 y=0,
所以 CSS 里要配半透明填充:path { fill: rgba(100%,90%,90%); }配置方法对照:x()(自变量)、y1()(顶线)、y0()(基线,默认 0);
纵向图表换 y()+x0()/x1()。两个独门工具10:
- 变基线:
y0(d => Math.random() * d[1])或y0(d => scale.y(d[2]))—— 区间带(最高-最低温度、置信区间)就是这么画的; - 抽边线:
area.lineY1()返回顶线的 line 生成器,area.lineY0()返回基线的—— 同一个 area 配置,顺手画出配套的两条边界线,不怕配置不同步。
多序列面积 = 多序列线图的 d3.line 换成 d3.area、配 y0(scale.y(0))
(y 轴反装后基线要显式指到 0 的像素位置)。默认是重叠摆放;堆叠留给第 12 章的 stack 生成器11。
2.6 径向版:同样的函数,极坐标参数
lineRadial / areaRadial 把 x/y 换成 angle/radius(角度用弧度、0 在 12 点钟方向)12:
const line = d3.lineRadial()
.angle(d => scale.angle(d[0])) // 月份 → 0~2π
.radius(d => scale.radius(d[1])); // 数值 → 像素
const area = d3.areaRadial()
.angle(d => scale.angle(d[0]))
.outerRadius(d => scale.radius(d[1])) // 顶线
.innerRadius(d => scale.radius(d[2])); // 基线(可省,默认 0)
// 轴用第 08 章的 utils.radialAxes();点标记要 Cartesian 坐标:
d3.pointRadial(scale.angle(a), scale.radius(r)) // → [x, y]两类容器定位法(原书都给了):translate 一个 <g> 到画布中心,或给 <svg> 设
viewBox="-w/2 -h/2 w h" 让原点天然在中心。后者省事,但「加图例会更难」13。
2.7 符号:分类标记的 14 种形状
d3.symbol() 生成以 (0,0) 为中心的符号路径,配 transform="translate(x,y)" 落位:
const star = d3.symbol(d3.symbolStar, 100); // 造一个 100 平方像素的星
star() // 'M4.514,0A4.514,…' ← path 字符串
// 按序列换形状:符号本身也可以当序数比例尺的 range
const symbolScale = d3.scaleOrdinal(d3.symbolsFill).domain([0, datasets.length]);
.attr("d", point.type(symbolScale(i))) // 第 i 个序列用第 i 种符号要点:size() 的单位是平方像素,且多数符号的实际占地与同面积正方形不同(只有
symbolSquare 恰好相等);14 种符号分两族 d3.symbolsFill / d3.symbolsStroke
(分别适合填充/描边);符号做分类标记是无障碍手段——色盲读者分不清颜色时,还分得清形状14。
3. 在线教程:全球变暖可视化
NASA GISS 的 1880–2024 月度温度距平数据,两条路线: Cartesian 版画每年一条线的多序列线图(悬停某年高亮该年);径向版把同一数据画成 螺旋(radial line)——大部分代码与 Cartesian 版共用,改的是坐标系。两个成品都有 Observable 在线版15。
4. 作者的判断与证据
| 判断 | 性质 | 依据 |
|---|---|---|
| 「样条会发明数据里没有的极值」 | 方法学警告 | 曲线函数一节的定位:「intermediate values are estimates」9 |
| 「符号是无障碍手段」 | 设计声明 | 色盲读者的分类识别14 |
| 「直接函数引用 attr("d", line)」 | 风格建议 | 原书注明是 best practice 取向,非强制3 |
5. 边界与局限
- 自定义 curve(实现接口方法)与脱离 line/area 单独用 curve,原书明确「beyond the scope」9。
- 自定义 symbol 只在练习区给了例子,正文没教。
- 本章的「面积图」只有重叠式;堆叠面积是下一章的事。
digits对文件体积的影响只有定性描述,没有量化数字。
判断(我们的,不是书里的): 生成器函数是 D3 里最值得移植到自己代码里的模式: 「配置与执行分离」+「访问器约定」让一个函数同时服务多种数据形状(坐标对、对象、极坐标)。 它也是「数据驱动」思想的极致——图形不是被画出来的,是被数据调用出来的。 如果错,会错在: 生成器模式把「一个图形 = 一个函数调用」的粒度固定死了; 需要「图形的一部分」参与 join(如渐变着色的线段)时就得退回手动拼 path 或拆成多段—— 原书的多序列方案里,整根线只能有一色,想沿线变色要另想办法。
6. 可带走的
主线例子一行收束: [[0,0],[100,200],[200,50],[300,150],[400,100]] →
d3.line() 得 "M0,0L100,200L200,50L300,150L400,100" → datum 绑 <path> 成线;
换 d3.area() 同一输入得闭合面带(默认基线 y=0),换 .curve(d3.curveBasis) 变样条。
- 生成器两步走:造函数(
d3.line(),配 x/y 访问器)→ 调函数(数据进、path 字符串出); - 单条线用
datum()+attr("d", line);多条线用 join,外层绑「数组的数组」; - 比例尺写进 x()/y() 访问器;多序列的 domain 用
datasets.flat()一次 extent; defined()给缺数据留断口;digits(0)让坐标取整、字符串变短;- area 比 line 多一条基线:
y0可动(区间带),lineY1()/lineY0()抽出配套边线; - curve 选型的平衡:样条光滑但造伪极值,step 诚实但生硬;Monotone 系列保单调;
- 径向版参数是 angle(弧度)/radius,配
d3.pointRadial转 Cartesian; - 符号 size 是平方像素;符号族当序数尺的 range,按类别自动分形状——兼作色盲无障碍;
- 生成器不画图时:把函数单独调一次,看输出里有没有 NaN。
7. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| path 字符串与八生成器总表 | Creating Line and Area Charts | text/18-ch11-chapter-11-creating-line-and-area-charts.txt:39(搜「M280,160」) · text/18-ch11-chapter-11-creating-line-and-area-charts.txt:84(搜「Generator」) |
| 主线例子:line(coords) | Creating Line and Area Charts | text/18-ch11-chapter-11-creating-line-and-area-charts.txt:135(搜「[0,0], [100,200]」) · text/18-ch11-chapter-11-creating-line-and-area-charts.txt:139(搜「M0,0L100,200」) |
| datum() 与直接函数引用 | Creating Line and Area Charts | text/18-ch11-chapter-11-creating-line-and-area-charts.txt:166(搜「datum(coords)」) · text/18-ch11-chapter-11-creating-line-and-area-charts.txt:169(搜「Important note」) |
| line 配置方法与对象/比例尺 | Creating Line and Area Charts | text/18-ch11-chapter-11-creating-line-and-area-charts.txt:241(搜「x(function)」) · text/18-ch11-chapter-11-creating-line-and-area-charts.txt:277(搜「d => d.step」) · text/18-ch11-chapter-11-creating-line-and-area-charts.txt:288(搜「scale.x(d[0])」) · text/18-ch11-chapter-11-creating-line-and-area-charts.txt:299(搜「defined(d => d」) |
| 多序列线图三层结构 | Creating Line and Area Charts | text/18-ch11-chapter-11-creating-line-and-area-charts.txt:340(搜「steps = d3.range(0,21,5)」) · text/18-ch11-chapter-11-creating-line-and-area-charts.txt:367(搜「datasets.flat()」) · text/18-ch11-chapter-11-creating-line-and-area-charts.txt:385(搜「selectAll("path.dataset")」) |
| 调试 path 生成器 | Creating Line and Area Charts | text/18-ch11-chapter-11-creating-line-and-area-charts.txt:413(搜「Debugging shape generators」) · text/18-ch11-chapter-11-creating-line-and-area-charts.txt:438(搜「console.log(d)」) |
| area:基线、闭合、抽边线 | Creating Line and Area Charts | text/18-ch11-chapter-11-creating-line-and-area-charts.txt:491(搜「M0,0L100,200」) · text/18-ch11-chapter-11-creating-line-and-area-charts.txt:505(搜「y1(d => d[1])」) · text/18-ch11-chapter-11-creating-line-and-area-charts.txt:512(搜「Math.random() * d[1]」) · text/18-ch11-chapter-11-creating-line-and-area-charts.txt:573(搜「lineY1()」) |
| 多序列面积与堆叠预告 | Creating Line and Area Charts | text/18-ch11-chapter-11-creating-line-and-area-charts.txt:609(搜「y0(scale.y(0))」) · text/18-ch11-chapter-11-creating-line-and-area-charts.txt:623(搜「Chapter 12」) |
| 曲线函数与参数 | Creating Line and Area Charts | text/18-ch11-chapter-11-creating-line-and-area-charts.txt:644(搜「curveBasis」) · text/18-ch11-chapter-11-creating-line-and-area-charts.txt:654(搜「(alpha), which」) · text/18-ch11-chapter-11-creating-line-and-area-charts.txt:665(搜「step curve」) |
| 径向线/面积与 pointRadial | Creating Line and Area Charts | text/18-ch11-chapter-11-creating-line-and-area-charts.txt:703(搜「lineRadial()」) · text/18-ch11-chapter-11-creating-line-and-area-charts.txt:719(搜「pointRadial(a,r)」) · text/18-ch11-chapter-11-creating-line-and-area-charts.txt:736(搜「2*Math.PI」) · text/18-ch11-chapter-11-creating-line-and-area-charts.txt:825(搜「areaRadial()」) |
| 径向多序列与 viewBox 原点 | Creating Line and Area Charts | text/18-ch11-chapter-11-creating-line-and-area-charts.txt:902(搜「viewBox」) · text/18-ch11-chapter-11-creating-line-and-area-charts.txt:924(搜「g.dataset」) |
| 符号与无障碍 | Creating Line and Area Charts | text/18-ch11-chapter-11-creating-line-and-area-charts.txt:957(搜「d3.symbol()」) · text/18-ch11-chapter-11-creating-line-and-area-charts.txt:963(搜「M4.514,0A4.514」) · text/18-ch11-chapter-11-creating-line-and-area-charts.txt:1003(搜「symbolStar」) · text/18-ch11-chapter-11-creating-line-and-area-charts.txt:1009(搜「square pixels」) · text/18-ch11-chapter-11-creating-line-and-area-charts.txt:954(搜「accessible」) |
| 全球变暖教程 | Creating Line and Area Charts | text/18-ch11-chapter-11-creating-line-and-area-charts.txt:1049(搜「global warming」) |
| 章小结 | Creating Line and Area Charts | text/18-ch11-chapter-11-creating-line-and-area-charts.txt:1070(搜「Summary」) |
Footnotes
-
出处:「Creating Line and Area Charts」第 79–100 段(
text/18-ch11-chapter-11-creating-line-and-area-charts.txt:84,搜「Generator」)。八个生成器见原书 Table 11.1;arc 与 link 的归属见第 94–98 段。 ↩ -
出处:「Creating Line and Area Charts」第 130–159 段(
text/18-ch11-chapter-11-creating-line-and-area-charts.txt:135,搜「[0,0], [100,200]」;:139,搜「M0,0L100,200」)。五坐标数组与 path 字符串照原书。 ↩ -
出处:「Creating Line and Area Charts」第 162–187 段(
text/18-ch11-chapter-11-creating-line-and-area-charts.txt:166,搜「datum(coords)」;:175,搜「c => line(c)」;:186,搜「Best practices usually recommend」)。 ↩ ↩2 -
出处:「Creating Line and Area Charts」第 235–258 段(
text/18-ch11-chapter-11-creating-line-and-area-charts.txt:241,搜「x(function)」;:245,搜「defined(function)」;:249,搜「digits(number)」)。配置表照原书 Table 11.2 转写。 ↩ -
出处:「Creating Line and Area Charts」第 260–307 段(
text/18-ch11-chapter-11-creating-line-and-area-charts.txt:277,搜「d => d.step」;:288,搜「scale.x(d[0])」;:293,搜「d3.line(d => scale.x」;:299,搜「defined(d => d」)。 ↩ -
出处:「Creating Line and Area Charts」第 309–410 段(
text/18-ch11-chapter-11-creating-line-and-area-charts.txt:340,搜「steps = d3.range(0,21,5)」;:367,搜「datasets.flat()」;:385,搜「selectAll("path.dataset")」)。「三层上下文」的调试讲解见第 421–441 段。 ↩ -
出处:「Creating Line and Area Charts」第 364–368 段(
text/18-ch11-chapter-11-creating-line-and-area-charts.txt:367,搜「datasets.flat()」)。 ↩ -
出处:「Creating Line and Area Charts」第 413–441 段(
text/18-ch11-chapter-11-creating-line-and-area-charts.txt:417,搜「NaN」;:438,搜「console.log(d) || line(d)」)。 ↩ -
出处:「Creating Line and Area Charts」第 637–675 段(
text/18-ch11-chapter-11-creating-line-and-area-charts.txt:644,搜「curveBasis」;:654,搜「(alpha), which」;:639,搜「intermediate values are estimates」;:673,搜「beyond the scope of this book」)。 ↩ ↩2 ↩3 -
出处:「Creating Line and Area Charts」第 467–543 段(
text/18-ch11-chapter-11-creating-line-and-area-charts.txt:491,搜「M0,0L100,200」;:504,搜「y1(d => d[1])」;:512,搜「Math.random() * d[1]」;:573,搜「lineY1()」)。配置表照原书 Table 11.3 转写。 ↩ -
出处:「Creating Line and Area Charts」第 599–623 段(
text/18-ch11-chapter-11-creating-line-and-area-charts.txt:609,搜「y0(scale.y(0))」;:623,搜「Chapter 12」)。 ↩ -
出处:「Creating Line and Area Charts」第 693–794 段(
text/18-ch11-chapter-11-creating-line-and-area-charts.txt:703,搜「lineRadial()」;:709,搜「radians」;:719,搜「pointRadial(a,r)」;:737,搜「2*Math.PI」)与第 805–883 段(:825,搜「areaRadial()」)。 ↩ -
出处:「Creating Line and Area Charts」第 895–915 段(
text/18-ch11-chapter-11-creating-line-and-area-charts.txt:902,搜「viewBox」;:905,搜「harder… to add legends」)。 ↩ -
出处:「Creating Line and Area Charts」第 952–1045 段(
text/18-ch11-chapter-11-creating-line-and-area-charts.txt:954,搜「accessible」;:963,搜「M4.514,0A4.514」;:1003,搜「symbolStar」;:1009,搜「square pixels」)。 ↩ ↩2 -
出处:「Creating Line and Area Charts」第 1049–1068 段(
text/18-ch11-chapter-11-creating-line-and-area-charts.txt:1050,搜「Goddard Institute」;:1060,搜「spiral」)。 ↩