编程语言研究书库

线图与面积图 — 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)3path 字符串的小数位数;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 系列阶梯:水平再垂直(中点/前/后三种变奏)——
curveBasisB 样条,不过控制点,最光滑beta() 权重
curveCardinalC 样条,过控制点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) 变样条。

  1. 生成器两步走:造函数(d3.line(),配 x/y 访问器)→ 调函数(数据进、path 字符串出);
  2. 单条线用 datum() + attr("d", line);多条线用 join,外层绑「数组的数组」;
  3. 比例尺写进 x()/y() 访问器;多序列的 domain 用 datasets.flat() 一次 extent;
  4. defined() 给缺数据留断口;digits(0) 让坐标取整、字符串变短;
  5. area 比 line 多一条基线:y0 可动(区间带),lineY1()/lineY0() 抽出配套边线;
  6. curve 选型的平衡:样条光滑但造伪极值,step 诚实但生硬;Monotone 系列保单调;
  7. 径向版参数是 angle(弧度)/radius,配 d3.pointRadial 转 Cartesian;
  8. 符号 size 是平方像素;符号族当序数尺的 range,按类别自动分形状——兼作色盲无障碍;
  9. 生成器不画图时:把函数单独调一次,看输出里有没有 NaN。

7. 原文地图

主题原书章原文位置
path 字符串与八生成器总表Creating Line and Area Chartstext/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 Chartstext/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 Chartstext/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 Chartstext/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 Chartstext/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 Chartstext/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 Chartstext/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 Chartstext/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 Chartstext/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」)
径向线/面积与 pointRadialCreating Line and Area Chartstext/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 Chartstext/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 Chartstext/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 Chartstext/18-ch11-chapter-11-creating-line-and-area-charts.txt:1049(搜「global warming」)
章小结Creating Line and Area Chartstext/18-ch11-chapter-11-creating-line-and-area-charts.txt:1070(搜「Summary」)

Footnotes

  1. 出处:「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 段。 ↩

  2. 出处:「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 字符串照原书。 ↩

  3. 出处:「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

  4. 出处:「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 转写。 ↩

  5. 出处:「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」)。 ↩

  6. 出处:「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 段。 ↩

  7. 出处:「Creating Line and Area Charts」第 364–368 段(text/18-ch11-chapter-11-creating-line-and-area-charts.txt:367,搜「datasets.flat()」)。 ↩

  8. 出处:「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)」)。 ↩

  9. 出处:「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

  10. 出处:「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 转写。 ↩

  11. 出处:「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」)。 ↩

  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()」)。 ↩

  13. 出处:「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」)。 ↩

  14. 出处:「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

  15. 出处:「Creating Line and Area Charts」第 1049–1068 段(text/18-ch11-chapter-11-creating-line-and-area-charts.txt:1050,搜「Goddard Institute」;:1060,搜「spiral」)。 ↩

On this page