跳转至

快速上手 — 一张柱状图的十六步

这一章讲什么: 用十六个步骤,从零做出一张可以从外部数据加载、点按钮切换、平滑动画过渡的柱状图。 它不深入任何工具,但把后面十章的每一件核心工具都拿出来用了一遍: 选择器与 join、比例尺、配色方案、数据加载、事件、过渡动画。 这一章读完,你就拥有了 D3 应用的完整骨架;后续章节就是把骨架上每块肌肉单独练一遍。

1. 主线例子:一张会动的柱状图

全章只做一个东西:横向柱状图。先给它一张「定妆照」,后面每一步都朝它走:

  数据(行星到太阳的平均距离,单位 AU,共 13 项)
      │
      ▼  d3.json 加载 → 提取 name 与 distance → 排序
  ┌────────────────────────────────────────────┐
  │ Mercury  ████████████ 0.39 AU              │
  │ Venus    ███████████████████████ 0.72 AU   │
  │ …        …                                 │
  │ Eris     █████████████…(最长,67.67 AU)     │
  │                                            │
  │ [Maximum] [Average] [Minimum]   ← 按钮     │
  └────────────────────────────────────────────┘
  点按钮 → 柱子宽度、数值标签、颜色平滑滑动到新数据(1 秒,逐根延迟)

  图说:数据从 JSON 文件来,图由 D3 生成,按钮触发更新,更新带过渡动画。

这就是全章的最终形态。下面把十六步分成四段走: 先画静态图(步骤 1–9)、再接外部数据(10–12)、再加交互与动画(13–16)、 最后工程化收尾(分离文件)加一道地图彩蛋。

2. 顶层全景:十六步在干什么

  第一段 静态图(1–9)
    ① SVG 手写原型        —— 先不碰 D3,把想要的图用标签画出来
    ② 重组:用 <g> 分组    —— 每个数据项一组,位置用 translate 相对化
    ③ 换成 D3 生成        —— join() 按数据造 <g>,translate 用索引算
    ④ 加柱子与标签        —— 数据经回调流进属性
    ⑤ 调试                —— 元素检查器、console.log、模块作用域的坑
    ⑥ 引入比例尺          —— 线性比例尺管宽度,band 比例尺管纵向排布
    ⑦ 加颜色              —— 序数比例尺 + Tableau 配色
    ⑧ 换对象数据          —— 真实数据是对象数组,属性要「访问器」去取
    ⑨ 数字格式化与排序    —— d3.format 与 d3.ascending
  第二段 外部数据(10–12)
    ⑩ 重构出 drawChart()  —— 渲染代码装进函数
    ⑪ 加载 JSON           —— d3.json() 一步完成「取文件 + 解析」
    ⑫ 从大文件里取子集     —— map 出自己想要的字段
  第三段 交互与动画(13–16)
    ⑬ 再重构:app 容器     —— 常量集中,支持多图表切换
    ⑭ 加数据、加按钮       —— Map 存图表配置,按钮渲染成表单
    ⑮ 事件更新            —— on('click') 触发数据重绑
    ⑯ 过渡动画            —— transition + duration + delay
  收尾
    分离 HTML/CSS/JS 模块  +  彩蛋:用 GeoJSON 画世界地图

  图说:每步只做一件事,且永远上一步的代码是下一步的地基——这正是可维护的改法。

3. 核心原理:四段逐段走

第一段 静态图:从手写到数据驱动

步骤 1:先手写 SVG 原型——「先画草图,再写代码」

原书的教法第一步不用 D3:先在纸上(或脑子里)画草图,然后直接用 SVG 标签把原型写出来。 目标是验证「这张图需要哪些元素、哪些属性」1。

画布 600×300 像素。柱子高 20 像素,纵向间隔 1 像素;左边留 50 像素放分类标签。 先写死三根柱子(宽度是随便挑的、能塞进画布的数):

  <svg id="bar-chart" width="600" height="300">
    <rect class="bar" x="50" y="0"  width="225"/>
    <rect class="bar" x="50" y="21" width="425"/>
    <rect class="bar" x="50" y="42" width="125"/>
  </svg>

  图说:rect 是矩形。x=50 让柱子避开左边的标签区;y 取 0、21、42,
  柱子 20 高,正好错出 1 像素缝隙。宽度 225/425/125 代表数值。

再配上标签文字和 CSS(柱子的橙色填充、标签的字号、分类标签右对齐), 一张静态原型就成了2。这一步的意义原书说得很白:确认每个标签、样式、属性值, 以及图为什么长这样——后面把它换成 D3 时,你才知道要生成什么3。

步骤 2:重组——<g> 分组 + translate 相对化

直接生成上面的平铺结构会很麻烦:每个 <rect>、<text> 的坐标都依赖它在列表里的序号。 原书的改法是把每个数据项的三个元素(柱子 + 两个标签)装进一个 <g> 组, 再给组挂 transform="translate(x,y)",组内坐标立刻全部相对化4:

  <!-- 平铺:绝对坐标,每项三个元素都要算总账 -->
  <rect class="bar" x="50" y="0" width="225"/>
  <text class="category" y="10" x="45">One</text>
  <text class="value" y="10" x="280">225</text>

  <!-- 分组:组负责「挪到第几行」,组内元素坐标永远不变 -->
  <g class="entry" transform="translate(50,0)">
    <rect class="bar" width="225"/>
    <text class="category" y="10" x="-5">One</text>
    <text class="value" y="10" x="230">225</text>
  </g>

  图说:translate 把组的局部坐标原点挪到 (50, 行高×序号)。第二根组的 translate 是 (50,21),
  组内代码与第一根一模一样。原来三个「随序号变化」的属性,现在只剩 translate 一个。

这一步之后,组内所有属性都变成了静态值,唯一随数据项变化的是组的 translate 和两个数值 (柱宽、值标签的 x 和文本)5。这正是把图「程序化」的关键准备:变化点越少,后面用回调函数(交给框架的函数,框架轮到每个元素时替你调用)接管越容易。

步骤 3:换成 D3——join() 第一次实战

现在把 HTML 里的 <svg> 内容清空,引入 D3,写生成代码:

  const dataset = [225, 425, 125, 50, 450, 75, 325];
  const svg = d3.select("#bar-chart");

  svg.selectAll(".entry")
    .data(dataset)
    .join("g").attr("class", "entry")
      .attr("transform", (_, index) => `translate(50, ${index * 21})`);

  图说:(_, index) 里的下划线是「这个参数我不用的」约定写法;
  这里只用第二个参数——数组索引。translate(50, 索引×21) 就是步骤 2 里的三种位移。

和步骤 2 的手写版对照,发生的变化是:

手写版 D3 版
三个 <g> 逐个写 join("g") 按数组长度自动生成 7 个
translate 的 y 写死 0/21/42 回调 (_, index) => index * 21 按索引算
数据(225、425…)散在属性里 数据集中在数组里,绑上去6

join() 生成 7 个 <g class="entry">,每个自动绑上数组里的一项; 此时页面还是空白,因为组里还是空的7。可以在浏览器的元素检查器里看到生成的代码—— 这也是原书反复强调的调试习惯:图没出来,先看代码生成了没有8。

步骤 4:往组里塞内容——数据经回调流进属性

把选择存进常量 entries,然后往每个组里依次追加柱子、分类标签、值标签:

  const entries = svg.selectAll(".entry")
    .data(dataset)
    .join("g").attr("class", "entry")
      .attr("transform", (_,i) => `translate(50, ${i * 21})`);

  entries.append("rect").attr("class", "bar")
    .attr("width", d => d);                       // 柱宽 = 数据值

  entries.append("text").attr("class", "category")
    .attr("x", -5).attr("y", 10)
    .text((_,i) => `Item ${1 + i}`);              // "Item 1" "Item 2"…

  entries.append("text").attr("class", "value")
    .attr("x", d => d + 5).attr("y", 10)
    .text(d => d);                                // 文本 = 数据值

  图说:追加的三个子元素按调用顺序依次落进每个组。

两个细节值得记住:

第一,数据会遗传。 entries 上绑的数组,被子元素 <rect>、<text> 原样继承—— 所以子元素的回调里照样拿得到 d(当前数据项)和 i(索引)9。

第二,text() 设文本内容,attr() 设标签属性——两者别混。 text(d => d) 把数据值写进标签;attr("x", d => d + 5) 把值标签放在柱子右端 5 像素处。

至此静态图完成。刷新页面,7 根柱子带标签。但图还很糙:数值大时柱子可能超出画布, 纵向空间也浪费大半。接下来两步解决。

步骤 5:调试——新手的救生索

图不对甚至空白时怎么办?原书给了一组马上能用的招10:

症状 查哪里
页面空白、无任何报错 元素检查器:代码生成了吗?元素坐标是不是画到屏幕外了?
控制台报错 报错信息里浏览器给了出错的行;常见外部原因:文件缺失、路径错、网络问题
数据内容存疑 console.log('dataset', dataset)——第一个参数写描述性字符串,多个 log 才不混
log 的是 D3 对象看不懂 .node() 转成 DOM 节点再 log:console.log('<svg>', svg.node())
想在控制台里敲 D3 命令 模块封装后控制台访问不到模块内的变量——临时代价:window.d3 = d3,或代码末尾加 debugger; 暂停后控制台进入模块作用域11

步骤 6:引入比例尺——把数据世界翻译成像素世界

柱子长度直接拿数据值当像素用(225 就 225 像素)有两个毛病: 最大值换一批数据就顶破画布;小数值的柱子短得看不见。比例尺(scale)就是干翻译的: 把数据的取值范围(domain)按比例映射到画布的尺寸范围(range)12。

先声明画布的尺寸对象,顺手把 <h1> 和 <svg> 也改成用 D3 生成(练习「一切皆可用代码造」):

  const dim = {width: 600, height: 300};
  const body = d3.select("body");
  body.append("h1").text("Bar chart");
  const svg = body.append("svg")
    .attr("id", "bar-chart")
    .attr("width", dim.width)
    .attr("height", dim.height);

横向:线性比例尺。 d3.scaleLinear() 造一个线性函数,输入 domain 里的值,返回 range 里的值:

  const xscale = d3.scaleLinear()
    .range([0, dim.width - 100])          // 输出:0 ~ 500 像素(留 100 给标签)
    .domain([0, d3.max(dataset)]);        // 输入:0 ~ 450(数组里的最大值)

  xscale(0)    // 0
  xscale(450)  // 500 —— 最大值正好顶到 500 像素

  图说:d3.max(dataset) 取数组最大值 450。线性比例尺就是一条 f(x)=ax+b 的直线,
  把 [0,450] 均匀压到 [0,500]。每处用数据值的地方换成 xscale(d),柱子和值标签一起缩放。

纵向:band 比例尺。 柱子的纵向位置不该手算 index * 21——那是为固定行高凑的数。 d3.scaleBand() 是分类专用的比例尺:domain 放整个数组(每项一个「带」), range 放画布高度,padding(0.1) 让带与带之间留 10% 的空隙13:

  const yscale = d3.scaleBand()
    .range([0, dim.height])     // 输出:0 ~ 300 像素
    .domain(dataset)            // 输入:数组的每一项各占一条带
    .padding(0.1);              // 带间 10% 留白

  const barHeight = yscale.bandwidth();  // 每条带的宽度 = 柱高
  yscale(dataset[0])   // 第一根柱子的 y 坐标
  yscale(dataset[6])   // 最后一根柱子的 y 坐标

  图说:bandwidth() 返回每条带的实际像素宽度——柱高从此由比例尺决定,
  CSS 里写死 height:20px 的那行要删掉,两边只能留一个说了算的。

用了比例尺之后,translate、柱高、标签纵向居中全部换成比例尺的输出。 这时再改数据——原书试了加 4 个数变成 11 项——柱子自动重新排布、自动适配高度14。 这就是比例尺的回报:数据随便变,图自己跟上。

步骤 7:加颜色——序数比例尺 + 现成配色方案

去掉 CSS 里写死的橙色,换成分类配色方案 d3.schemeTableau10(Tableau 软件那套 10 色, D3 内置)。工具还是比例尺,只是这次输入是序号、输出是颜色字符串——序数比例尺(scaleOrdinal):

  const color = d3.scaleOrdinal(d3.schemeTableau10).domain(yscale.domain());

  color(5)   // 返回 '#af7aa1'(方案里的第 6 个颜色)

  entries.append("rect").attr("class", "bar")
    .attr("width", d => xscale(d))
    .style("fill", (d,i) => color(i));     // 第 i 根柱子染第 i 个颜色

  图说:配色方案是一个颜色字符串数组;scaleOrdinal 把数组下标映射到颜色。
  数据超过 10 项时颜色自动循环。

一个细节:style("fill", …) 写的是 CSS 样式,优先级高于外部样式表里写的 .bar { fill: orange }; 如果改用 attr("fill", …) 写属性,外部样式表反而压不过它——所以 D3 里设颜色用 style() 更保险15。

步骤 8:换对象数据——真实世界的数据不止一个数

前面用的是数字数组,真实数据通常是对象数组。换成太阳系 14 个天体的数据(名字 + 到太阳的平均距离,单位天文单位 AU):16

  const dataset = [
    {name: "Mercury", distance: 0.387}, {name: "Venus",   distance: 0.723},
    {name: "Earth",   distance: 1},     {name: "Mars",    distance: 1.52},
    /* …木星 5.2、土星 9.54、天王星 19.2、海王星 30.1… */
    {name: "Pluto",   distance: 39.481},{name: "Eris",    distance: 67.67}
  ];

直接把数组塞进 data() 会怎样?图坏了,而且不报错。 因为绑到元素上的 d 从数字变成了对象,attr("width", d => d) 拿到的是对象, JavaScript 默默把它当字符串用,柱子全部塌成一个宽度17。

修法:凡是「要数值的地方」,改用访问器(accessor)从对象里取字段:

  const xscale = d3.scaleLinear().range([0, dim.width-100])
    .domain([0, d3.max(dataset, d => d.distance)]);   // 最大值也从字段里取

  entries.append("rect").attr("class", "bar")
    .attr("width", d => xscale(d.distance));           // 柱宽 ← distance 字段
  entries.append("text").attr("class", "category")
    .text(d => d.name);                                // 标签 ← name 字段
  entries.append("text").attr("class", "value")
    .attr("x", d => xscale(d.distance) + 5)
    .text(d => d.distance);

这一步还有个隐藏要点:距离值从 0.387 到 67.67,如果直接当像素,水星那根只有 0.387 像素—— 根本看不见。有了比例尺之后,数据是什么量级都无所谓,domain 一配,自动铺满画布18。

步骤 9:数字格式化与排序——让标签像人话

值标签直接打印 d.distance,0.387 后面拖一长串小数。 d3.format(".2f") 造一个格式化函数:.2f 的意思是「定点小数,保留 2 位」 (fmt(5.2) 得到 "5.20")。标签文本变成 ${fmt(d.distance)} AU,带上单位19。

排序用 JavaScript 自带的 sort(),比较函数借 D3 的 d3.ascending:

  dataset.sort((a,b) => d3.ascending(a.distance, b.distance));

柱子按距离从近到远重排——因为 y 坐标来自 yscale(d),排序后的数组让 join 按新顺序重绑20。

第二段 外部数据:图从文件里长出来

步骤 10:重构出 drawChart()

渲染代码越堆越长,先装进一个函数,参数是数据集:

  drawChart(planets);            // 调用

  function drawChart(dataset) {
    dataset.sort((a,b) => d3.ascending(a.distance, b.distance));
    /* …渲染代码全部搬进来… */
  }

步骤 11:加载 JSON——d3.json() 一步搞定

数据挪进独立的 dataset.json(和 HTML 同目录),代码里只剩一句:

  d3.json(url).then(data => drawChart(data));

  // 或者等价写法
  const data = await d3.json(url);
  drawChart(data);

d3.json() 把「请求文件 + 解析 JSON」合成一步,返回 Promise——这就是第 02 章讲的那套异步机制21。 最常见的翻车:404 Not Found(路径错、文件名错),以及没用 Web 服务器、直接双击打开 HTML 文件 ——浏览器禁止页面从本地文件系统抓数据,必须走 http://22。数据到手先 console.log(data) 验货,再往下走23。

步骤 12:从大文件里取子集——map 出自己想要的形状

真实文件往往比图需要的多。换成更大的 sol_2019.json:根对象下有 6 个顶级属性 (star、planets、asteroids、tnos、centaurs、comets),每个数组里的对象有十几个字段, 而图只需要 name 和 semiMajorAxisAU 两个24。

加载后多一步加工:用 map() 把每个对象投影成图认识的形状:

  d3.json(url).then(data => {
    const distances = data.planets.map(object =>
      ({ name: object.name, distance: object.semiMajorAxisAU }));
    drawChart(distances);
  });

  图说:data.planets 是「planets 数组」;map 逐个取出两个字段,组成 {name, distance} 新数组——
  形状和上一节手写的数组一模一样,drawChart 原样接手。

第三段 交互与动画:让图动起来

步骤 13:再重构——app 容器与多图表配置

要支持「平均 / 最远 / 最近」三种视图,代码再整理一次:常量收进一个 app 对象, 图表配置收进一个 Map(键是 'avg'、'min'、'max',值是 {title: 'Average'} 这类配置), 用 app.current 记当前显示哪个25:

  const app = {
    current: 'avg',
    data: [],
    xscale: d3.scaleLinear().range([0, dim.width-140]),  // 只配 range,domain 等数据来了再说
    yscale: d3.scaleBand().range([0, dim.height]).padding(0.1),
    color: d3.scaleOrdinal(d3.schemeTableau10),
    fmt: d3.format(".2f")
  };
  app.charts = new Map();
  app.charts.set("avg", {title: 'Average'});
  app.charts.set("min", {title: 'Minimum'});
  app.charts.set("max", {title: 'Maximum'});

注意比例尺的range 先配好、domain 后配——domain 依赖数据,数据没来之前定了也没用, 这是比例尺的常规用法26。

数据处理也函数化:makeDataset(data, 'planets') 从大 JSON 里取 planets 数组, 顺手做个类型转换——JSON 里的数字读出来默认是字符串,前面加 + 转成数:

  function makeDataset(data, section) {
    data[section].forEach(object => {
      app.data.push({ name: object.name, avg: +object.semiMajorAxisAU });
    });
  }
  // app.data → [{name: "Mercury", avg: 0.387}, {name: "Venus", avg: 0.723}, …]

渲染时用 d[app.current] 代替 d.avg——中括号里放变量,一个写法通吃三种图表27。

步骤 14:按钮面板——用数据生成控件

三个按钮本身就是一张「配置表驱动的界面」。renderControls() 遍历 app.charts, 每个键值对生成一个按钮,id 用键、文字用标题,当前图表的按钮禁用:

  function renderControls() {
    const form = body.append("form");
    app.charts.forEach((value, key) => {
      form.append("button").attr("type", "button").attr("id", key)
        .property("disabled", app.current === key)
        .text(value.title);
    })
  }

.property() 设的是 DOM 属性(disabled 是属性不是标签属性,要用 property 而不是 attr)。 按钮渲染出来了,点击还没反应——下一步接事件28。

步骤 15:点击事件——重绑数据与局部更新

给按钮挂上 'click' 监听:.on("click", update)。 事件处理函数 update(event) 拿到 DOM 事件,event.target.id 就是被点按钮的 id ('avg'/'min'/'max'),用它更新 app.current,然后分两层更新29:

界面层:换标题、放开所有按钮再禁用当前那个。 图表层:重新排序 + 重绑数据,再更新柱子宽度、标签:

  function update(event) {
    app.current = event.target.id;
    d3.select("#title").text(app.charts.get(app.current).title);
    d3.selectAll("button").property("disabled", false);
    d3.select(`#${app.current}`).property("disabled", true);

    sortData();                       // 按新指标重排序
    const entries = body.select("#bar-chart").selectAll(".entry")
      .data(app.data);                // 顺序变了,必须重绑

    entries.select(".bar")            // select 进到每个组里已有的柱子
      .style("fill", d => d.color)    // 顺序变了,颜色也要跟着换
      .attr("width", d => app.xscale(d[app.current]));
    entries.select(".category").text(d => d.name);
    entries.select(".value")
      .attr("x", d => app.xscale(d[app.current]) + 5)
      .text(d => `${app.fmt(d[app.current])} AU`);
  }

两个容易漏的点,原书都点了出来:

  • 顺序变了就必须重绑:.data(app.data) 不是可有可无——数组顺序变了,元素与数据项的对应关系变了30。
  • 比例尺的 domain 要放宽:xscale 之前按 avg 的最大值(39.481)配的,切到 max 后最大值 45.346 会顶破边。domain 改成三个字段一起取最大:d3.max(app.data, d => d3.max([d.avg, d.max, d.min]))31。

注意这里更新已有元素用的不是 join,而是 entries.select(".bar")—— 在已绑数据的父选择器上 select 子元素,子元素继承父元素的数据,所以回调里照样有 d。 这是第 05 章「嵌套选择」的雏形。

步骤 16:过渡动画——一行 transition,再一行 delay

变化是瞬间跳变的,最后加一行动画。在重绑数据之后插入:

  const entries = body.select("#bar-chart").selectAll(".entry")
    .data(app.data)
    .transition();                       // 后面的属性修改全部变成动画

点按钮,柱子、标签平滑滑向新值,默认时长 ¼ 秒32。再加两个旋钮:

  .transition().duration(1000);              // 时长 1 秒
  .transition().duration(1000).delay((_,i) => i*50);   // 第 i 根延迟 i×50 毫秒,逐根起舞

delay() 可以传固定毫秒数,也可以传回调——按索引递增延迟,7 根柱子就有「依次跟上」的波浪效果33。

收尾:工程化 + 地图彩蛋

分离 HTML/CSS/JS。 真实项目不该把三种代码塞一个文件。原书解释了为什么教学时反着来 (例子小、模块封装方便控制台实验、少踩加载的坑),但最后演示了正经拆法:

  index.html        只有 HTML,<link> 引 css/main.css,<script> 引 js/main.js
  js/constants.js   import d3 + 定义 dim/app,export {dim, app}
  js/render.js      import {dim, app},export renderChart() 和 renderControls()
  js/main.js        唯一的入口:await 数据、makeDataset、调渲染

  图说:ES 模块的 import/export 把依赖关系画清楚;每个文件只干一类事。

main.js 顶层直接用 await(模块本身支持顶层 await),不再写 .then()34。

地图彩蛋。 章末附了一个在线教程:用同一个 GeoJSON 文件,把国家边界的经纬度坐标 投影成 SVG 多边形,画一张世界地图35。正式的地图工具(d3-geo:投影、球面几何)留到第 20、21 章。

4. 作者的判断与证据

这一章是教程,判断少,但教学决策本身是可分析的:

决策 性质 依据
「先手写 SVG 原型,再逐段换 D3」 教学主张 理由:先弄清要生成什么,才知道代码写得对不对3
「教学例子全塞一个文件」 权衡说明 三条理由都给了(例子小、要控制台实验、避免加载坑)36
「用 style() 而不是 attr() 设颜色」 经验规则 理由:CSS 优先级——style 属性能压过样式表,attr 不能15
「顺序变了必须重绑、domain 要覆盖全部视图」 易错点提示 书中用「Pluto 排在 Neptune 后」这类具体错位演示后果30

5. 边界与局限

  • 比例尺、join、事件在本章都是「先用起来」,机制在后续章节才拆:比例尺第 09 章、 join 细节第 05 章、颜色第 10 章、事件第 14–15 章、过渡第 13 章。
  • 没有坐标轴——整张图没有一根轴线和刻度,坐标轴要到第 08 章才出现。
  • 调试一节只覆盖入门场景;选择器层级的调试在第 05 章末尾专门有一节。
  • 「彩蛋」地图教程不在纸质书里,在仓库 Part1/Chapter04/Tutorial/,纸质版只有说明和一张成品图35。
  • 全部 16 步的代码在仓库 1_BarStatic/、2_BarData/、3_BarDynamic/、4_BarFinal/ 四个文件夹里逐级演进37。

判断(我们的,不是书里的): 这一章最有价值的不是柱状图本身,而是它示范的改代码的节奏: 每一步小到什么程度、什么时候重构、什么时候先跑通再加功能—— 「先手写原型再程序化」这条尤其值得带走:它把「 declarative 生成代码」(声明目标结构) 和「手工结构」对照起来,差异点(变化点)自己浮出来,而这些差异点正是回调函数要接管的地方。 如果错,会错在: 对非常有经验、心里已有图的开发者,手写原型这一步可能是多余的—— 但对学习和排错场景,它几乎总是划算的。

6. 可带走的

本章主线例子,一行走完: [225, 425, 125, 50, 450, 75, 325] 七个数 → join 出 7 组 <g translate(50, y)> → 组内 rect/text 用回调取数 → scaleLinear 译宽度、scaleBand 排纵向 → 换行星对象数组(访问器取字段)→ d3.json 从文件加载 → 按钮 'click' 换 app.current 重绑 → transition().duration(1000).delay(i*50) 动画过去。

  1. 先手写原型再程序化——变化点自己浮出来,回调函数接管变化点;
  2. <g> + translate 让组内坐标相对化,是柱状图、树图、地图都用的结构手法;
  3. join() 按数据造元素,数据遗传给子元素,子元素的回调拿得到 d 和 i;
  4. select(子元素) 用于更新已有图形——继承父级数据,不必重新 join;
  5. 比例尺 = 把 domain(数据范围)翻译成 range(像素范围);range 先配、domain 数据来了再配;
  6. d3.max(数组, 访问器) 配合对象数据;+字段 把 JSON 读出的字符串转回数字;
  7. d3.format(".2f") 数字格式化;d3.ascending 当 sort 的比较函数;
  8. 颜色:d3.scaleOrdinal(d3.schemeTableau10);设色用 .style("fill") 不用 attr;
  9. d3.json(url).then(...) / await d3.json(url) 一步加载解析;404 和「没走服务器」是两大翻车点;
  10. 交互更新三件事:换状态 → 重绑数据(顺序变时必须)→ 更新属性;动画加 .transition().duration().delay()。

7. 原文地图

主题 原书章 原文位置
本章目标与四段结构 Quick Start text/10-ch04-chapter-4-quick-start.txt:2(搜「step-by-step」) · text/10-ch04-chapter-4-quick-start.txt:13(搜「static bar chart」)
步骤 1 原型(rect/text/CSS) Quick Start text/10-ch04-chapter-4-quick-start.txt:48(搜「bar-chart" width="600"」) · text/10-ch04-chapter-4-quick-start.txt:84(搜「y="0"」) · text/10-ch04-chapter-4-quick-start.txt:106(搜「class="category"」)
步骤 2 分组与 translate Quick Start text/10-ch04-chapter-4-quick-start.txt:141(搜「grouped in SVG」) · text/10-ch04-chapter-4-quick-start.txt:169(搜「translate(50,0)」)
步骤 3 换 D3:join 与 transform 回调 Quick Start text/10-ch04-chapter-4-quick-start.txt:208(搜「225, 425, 125」) · text/10-ch04-chapter-4-quick-start.txt:225(搜「join("g")」) · text/10-ch04-chapter-4-quick-start.txt:243(搜「index * 21」)
步骤 4 数据遗传、text()、d => d Quick Start text/10-ch04-chapter-4-quick-start.txt:254(搜「entries = svg.selectAll」) · text/10-ch04-chapter-4-quick-start.txt:266(搜「inherited」) · text/10-ch04-chapter-4-quick-start.txt:281(搜「Item ${1 + i}」)
步骤 5 调试(console/node()/window/debugger) Quick Start text/10-ch04-chapter-4-quick-start.txt:300(搜「Debugging」) · text/10-ch04-chapter-4-quick-start.txt:318(搜「node()」) · text/10-ch04-chapter-4-quick-start.txt:343(搜「encapsulated in modules」) · text/10-ch04-chapter-4-quick-start.txt:353(搜「debugger;」)
步骤 6 比例尺 Quick Start text/10-ch04-chapter-4-quick-start.txt:375(搜「dim = {width」) · text/10-ch04-chapter-4-quick-start.txt:395(搜「scaleLinear」) · text/10-ch04-chapter-4-quick-start.txt:429(搜「scaleBand」) · text/10-ch04-chapter-4-quick-start.txt:436(搜「bandwidth」) · text/10-ch04-chapter-4-quick-start.txt:467(搜「27, 690, 880」)
步骤 7 颜色方案与 style() Quick Start text/10-ch04-chapter-4-quick-start.txt:482(搜「schemeTableau10」) · text/10-ch04-chapter-4-quick-start.txt:488(搜「af7aa1」) · text/10-ch04-chapter-4-quick-start.txt:499(搜「cannot be overridden」)
步骤 8 对象数据与访问器 Quick Start text/10-ch04-chapter-4-quick-start.txt:516(搜「Mercury」) · text/10-ch04-chapter-4-quick-start.txt:537(搜「silently ignored」) · text/10-ch04-chapter-4-quick-start.txt:543(搜「d.distance」) · text/10-ch04-chapter-4-quick-start.txt:550(搜「d3.max(dataset, d => d.distance)」)
步骤 9 格式化与排序 Quick Start text/10-ch04-chapter-4-quick-start.txt:593(搜「d3.format(".2f")」) · text/10-ch04-chapter-4-quick-start.txt:607(搜「d3.ascending」)
步骤 10–12 加载与取子集 Quick Start text/10-ch04-chapter-4-quick-start.txt:642(搜「drawChart(dataset)」) · text/10-ch04-chapter-4-quick-start.txt:664(搜「d3.json(url).then」) · text/10-ch04-chapter-4-quick-start.txt:681(搜「404」) · text/10-ch04-chapter-4-quick-start.txt:702(搜「"star": {...}」) · text/10-ch04-chapter-4-quick-start.txt:746(搜「distances = data.planets.map」)
步骤 13–14 app 容器、按钮 Quick Start text/10-ch04-chapter-4-quick-start.txt:782(搜「const app =」) · text/10-ch04-chapter-4-quick-start.txt:816(搜「makeDataset」) · text/10-ch04-chapter-4-quick-start.txt:872(搜「charts.set("min"」) · text/10-ch04-chapter-4-quick-start.txt:908(搜「renderControls」)
步骤 15 事件与重绑 Quick Start text/10-ch04-chapter-4-quick-start.txt:943(搜「on("click", update)」) · text/10-ch04-chapter-4-quick-start.txt:951(搜「event.target.id」) · text/10-ch04-chapter-4-quick-start.txt:987(搜「needs update, because order changed」) · text/10-ch04-chapter-4-quick-start.txt:1006(搜「d3.max([d.avg, d.max, d.min])」)
步骤 16 过渡与延迟 Quick Start text/10-ch04-chapter-4-quick-start.txt:1015(搜「transition() method」) · text/10-ch04-chapter-4-quick-start.txt:1029(搜「duration(1000)」) · text/10-ch04-chapter-4-quick-start.txt:1035(搜「i*50」)
分离文件与模块 Quick Start text/10-ch04-chapter-4-quick-start.txt:1054(搜「Separating scripts」) · text/10-ch04-chapter-4-quick-start.txt:1078(搜「constants.js」) · text/10-ch04-chapter-4-quick-start.txt:1147(搜「await d3.json」)
地图彩蛋与章小结 Quick Start text/10-ch04-chapter-4-quick-start.txt:1160(搜「map with polygons」) · text/10-ch04-chapter-4-quick-start.txt:1174(搜「Summary」)

  1. 出处:「Quick Start」第 41–45 段(text/10-ch04-chapter-4-quick-start.txt:41,搜「turn this sketch into code」)。原文:「Now let's turn this sketch into code by creating a prototype using SVG and CSS. This step is important to test the elements and attribute values that we expect D3 to generate.」 ↩

  2. 出处:「Quick Start」第 70–128 段(text/10-ch04-chapter-4-quick-start.txt:76,搜「.bar {」;:106,搜「One」;:122,搜「text-anchor」)。原型三个柱子的 y 坐标 0/21/42、间隔 1 像素、左边距 50 像素均见第 70–90 段。 ↩

  3. 出处:「Quick Start」第 134–135 段(text/10-ch04-chapter-4-quick-start.txt:134,搜「Be sure you understand each tag」)。原文:「Be sure you understand each tag, style, and attribute value, and why the chart looks the way it does. We will use this prototype as a base to generate the SVG code.」 ↩↩

  4. 出处:「Quick Start」第 141–166 段(text/10-ch04-chapter-4-quick-start.txt:141,搜「<g> container」;:164,搜「relative positions」)。原文:「By moving the entire <g> container with a translate() transform, we can set relative positions for the bar and text elements.」 ↩

  5. 出处:「Quick Start」第 186–188 段(text/10-ch04-chapter-4-quick-start.txt:186,搜「only dynamic attributes」)。原文:「Now, the only dynamic attributes in a group are the bar's width and parameters that depend on it.」 ↩

  6. 出处:「Quick Start」第 217–229 段(text/10-ch04-chapter-4-quick-start.txt:217,搜「selectAll(".entry")」;:227,搜「seven <g class="entry">」)。原文:「The join() method creates seven <g class="entry"> elements and appends them as children of <svg>. Each <g> is automatically bound to an element from the dataset array.」 ↩

  7. 出处:「Quick Start」第 233–247 段(text/10-ch04-chapter-4-quick-start.txt:233,搜「stacked up in the same (0,0)」;:247,搜「nothing is yet visible」)。 ↩

  8. 出处:「Quick Start」第 229、245 段(text/10-ch04-chapter-4-quick-start.txt:229,搜「element inspector」;:245,搜「identical to the ones from our SVG prototype」)。 ↩

  9. 出处:「Quick Start」第 266–269 段(text/10-ch04-chapter-4-quick-start.txt:266,搜「inherited by its child elements」)。原文:「The data bound to the entries selection (and <g> elements) is inherited by its child elements (<text> and <rect>). Their attr() methods can access this data via a callback function, where the first argument is the current datum.」 ↩

  10. 出处:「Quick Start」第 300–317 段(text/10-ch04-chapter-4-quick-start.txt:300,搜「Debugging」;:311,搜「console.log()」)。 ↩

  11. 出处:「Quick Start」第 342–365 段(text/10-ch04-chapter-4-quick-start.txt:343,搜「encapsulated in modules」;:353,搜「debugger;」;:361,搜「window.d3 = d3」)。 ↩

  12. 出处:「Quick Start」第 370–402 段(text/10-ch04-chapter-4-quick-start.txt:390,搜「domain (the dimensions of your data)」;:401,搜「returns 0」)。domain/range 的译名「定义域/值域」是补充(不在书里,来自通用知识),原书用的是 domain/range 原词。 ↩

  13. 出处:「Quick Start」第 425–442 段(text/10-ch04-chapter-4-quick-start.txt:425,搜「band scale」;:429,搜「scaleBand」;:432,搜「padding(0.1)」;:436,搜「bandwidth()」)。原文:「The best choice for bar charts is a band scale.」 ↩

  14. 出处:「Quick Start」第 465–470 段(text/10-ch04-chapter-4-quick-start.txt:467,搜「27, 690, 880」)。原文:「Add or remove data, and the chart will automatically resize.」新数组 11 项见第 467 段。 ↩

  15. 出处:「Quick Start」第 498–499 段(text/10-ch04-chapter-4-quick-start.txt:499,搜「cannot be overridden」)。原文:「While attr() achieves similar results in SVG, style() is preferable since external stylesheets or <style> blocks cannot be overridden by attr(), only by style().」 ↩↩

  16. 出处:「Quick Start」第 511–529 段(text/10-ch04-chapter-4-quick-start.txt:516,搜「Mercury」;:529,搜「Makemake」)。14 个天体的完整数组照抄原书;单位 AU(天文单位)的换算(1 AU ≈ 1.5 亿公里)是补充(不在书里,来自通用知识)。 ↩

  17. 出处:「Quick Start」第 535–541 段(text/10-ch04-chapter-4-quick-start.txt:537,搜「silently ignored」)。原文:「JavaScript silently ignored these conversion errors and displayed each object using its default String representation, via the text(d => d) methods.」 ↩

  18. 出处:「Quick Start」第 545–550 段(text/10-ch04-chapter-4-quick-start.txt:545,搜「practically invisible」)。原文:「Mercury's bar would be practically invisible at 0.387 pixels, while the largest bar would only be 67 pixels wide, using just over 10% of the space.」 ↩

  19. 出处:「Quick Start」第 590–600 段(text/10-ch04-chapter-4-quick-start.txt:593,搜「d3.format(".2f")」;:600,搜「AU」)。d3.format 的机制(格式说明符)在第 06 章展开。 ↩

  20. 出处:「Quick Start」第 603–608 段(text/10-ch04-chapter-4-quick-start.txt:603,搜「sort() method」;:607,搜「d3.ascending」)。 ↩

  21. 出处:「Quick Start」第 664–672 段(text/10-ch04-chapter-4-quick-start.txt:664,搜「d3.json(url).then」)。原文:「It uses d3.json() to fetch and parse the JSON data into a JavaScript object… After parsing, the object is sent to drawChart().」 ↩

  22. 出处:「Quick Start」第 681–683 段(text/10-ch04-chapter-4-quick-start.txt:681,搜「most common errors」)。原文:「Errors may also occur if you load the file directly from the file system without fetching it from a web server.」 ↩

  23. 出处:「Quick Start」第 684–685 段(text/10-ch04-chapter-4-quick-start.txt:684,搜「Call console.log(data)」)。 ↩

  24. 出处:「Quick Start」第 698–727 段(text/10-ch04-chapter-4-quick-start.txt:701,搜「"star": {...}」;:710,搜「13 objects」)。 ↩

  25. 出处:「Quick Start」第 774–803 段(text/10-ch04-chapter-4-quick-start.txt:782,搜「const app =」;:799,搜「new Map()」)。 ↩

  26. 出处:「Quick Start」第 794–795 段(text/10-ch04-chapter-4-quick-start.txt:794,搜「only the ranges were configured」)。 ↩

  27. 出处:「Quick Start」第 850–862 段(text/10-ch04-chapter-4-quick-start.txt:851,搜「d[app.current]」;:861,搜「sortData」)。 ↩

  28. 出处:「Quick Start」第 900–933 段(text/10-ch04-chapter-4-quick-start.txt:908,搜「renderControls」;:912,搜「property("disabled"」)。按钮 HTML 原型见第 902–906 段。 ↩

  29. 出处:「Quick Start」第 935–954 段(text/10-ch04-chapter-4-quick-start.txt:936,搜「on() method」;:950,搜「event.target.id」)。原文:「The event handler receives the current DOM event, gets the clicked button (as the event's target property) and its id.」 ↩

  30. 出处:「Quick Start」第 979–998 段(text/10-ch04-chapter-4-quick-start.txt:980,搜「Pluto is placed after Neptune」;:987,搜「needs update, because order changed」)。原文:「To re-order the bars by new distances, first sort the data, then rebind it.」 ↩↩

  31. 出处:「Quick Start」第 1002–1006 段(text/10-ch04-chapter-4-quick-start.txt:1006,搜「d3.max([d.avg, d.max, d.min])」)。 ↩

  32. 出处:「Quick Start」第 1014–1025 段(text/10-ch04-chapter-4-quick-start.txt:1015,搜「transition() method」)。原文:「Instead of simply swapping the old data with the new data, a transition interpolates intermediate values during a quarter second.」 ↩

  33. 出处:「Quick Start」第 1026–1035 段(text/10-ch04-chapter-4-quick-start.txt:1029,搜「duration(1000)」;:1035,搜「i*50」)。原文:「each bar's delay is based on its index, creating a sequential animation」。 ↩

  34. 出处:「Quick Start」第 1072–1156 段(text/10-ch04-chapter-4-quick-start.txt:1078,搜「constants.js」;:1094,搜「js/main.js」;:1147,搜「await d3.json」;:1154,搜「using await instead of then()」)。 ↩

  35. 出处:「Quick Start」第 1160–1172 段(text/10-ch04-chapter-4-quick-start.txt:1160,搜「map with polygons」;:1164,搜「Tutorial/」)。原文:「The step-by-step tutorial is available from this book's GitHub repository in the Part1/Chapter04/Tutorial/ folder.」 ↩↩

  36. 出处:「Quick Start」第 1055–1069 段(text/10-ch04-chapter-4-quick-start.txt:1057,搜「single file」)。三条理由原文逐条列出(例子小好组织、模块封装方便控制台实验、避免加载失败干扰)。 ↩

  37. 出处:「Quick Start」第 24–27 段(text/10-ch04-chapter-4-quick-start.txt:24,搜「four sections」);四个代码文件夹名散见各步骤引用(如 text/10-ch04-chapter-4-quick-start.txt:1156,搜「4_BarFinal」)。 ↩