快速上手 — 一张柱状图的十六步¶
这一章讲什么: 用十六个步骤,从零做出一张可以从外部数据加载、点按钮切换、平滑动画过渡的柱状图。 它不深入任何工具,但把后面十章的每一件核心工具都拿出来用了一遍: 选择器与 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:
柱子按距离从近到远重排——因为 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) 动画过去。
- 先手写原型再程序化——变化点自己浮出来,回调函数接管变化点;
<g>+translate让组内坐标相对化,是柱状图、树图、地图都用的结构手法;join()按数据造元素,数据遗传给子元素,子元素的回调拿得到d和i;select(子元素)用于更新已有图形——继承父级数据,不必重新 join;- 比例尺 = 把 domain(数据范围)翻译成 range(像素范围);range 先配、domain 数据来了再配;
d3.max(数组, 访问器)配合对象数据;+字段把 JSON 读出的字符串转回数字;d3.format(".2f")数字格式化;d3.ascending当 sort 的比较函数;- 颜色:
d3.scaleOrdinal(d3.schemeTableau10);设色用.style("fill")不用 attr; d3.json(url).then(...)/await d3.json(url)一步加载解析;404 和「没走服务器」是两大翻车点;- 交互更新三件事:换状态 → 重绑数据(顺序变时必须)→ 更新属性;动画加
.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」) |
-
出处:「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.」 ↩ -
出处:「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 段。 ↩ -
出处:「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.」 ↩↩ -
出处:「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.」 ↩ -
出处:「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.」 ↩ -
出处:「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.」 ↩ -
出处:「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」)。 ↩ -
出处:「Quick Start」第 229、245 段(
text/10-ch04-chapter-4-quick-start.txt:229,搜「element inspector」;:245,搜「identical to the ones from our SVG prototype」)。 ↩ -
出处:「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.」 ↩ -
出处:「Quick Start」第 300–317 段(
text/10-ch04-chapter-4-quick-start.txt:300,搜「Debugging」;:311,搜「console.log()」)。 ↩ -
出处:「Quick Start」第 342–365 段(
text/10-ch04-chapter-4-quick-start.txt:343,搜「encapsulated in modules」;:353,搜「debugger;」;:361,搜「window.d3 = d3」)。 ↩ -
出处:「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 原词。 ↩ -
出处:「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.」 ↩ -
出处:「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 段。 ↩ -
出处:「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().」 ↩↩ -
出处:「Quick Start」第 511–529 段(
text/10-ch04-chapter-4-quick-start.txt:516,搜「Mercury」;:529,搜「Makemake」)。14 个天体的完整数组照抄原书;单位 AU(天文单位)的换算(1 AU ≈ 1.5 亿公里)是补充(不在书里,来自通用知识)。 ↩ -
出处:「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.」 ↩ -
出处:「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.」 ↩ -
出处:「Quick Start」第 590–600 段(
text/10-ch04-chapter-4-quick-start.txt:593,搜「d3.format(".2f")」;:600,搜「AU」)。d3.format的机制(格式说明符)在第 06 章展开。 ↩ -
出处:「Quick Start」第 603–608 段(
text/10-ch04-chapter-4-quick-start.txt:603,搜「sort() method」;:607,搜「d3.ascending」)。 ↩ -
出处:「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().」 ↩ -
出处:「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.」 ↩ -
出处:「Quick Start」第 684–685 段(
text/10-ch04-chapter-4-quick-start.txt:684,搜「Call console.log(data)」)。 ↩ -
出处:「Quick Start」第 698–727 段(
text/10-ch04-chapter-4-quick-start.txt:701,搜「"star": {...}」;:710,搜「13 objects」)。 ↩ -
出处:「Quick Start」第 774–803 段(
text/10-ch04-chapter-4-quick-start.txt:782,搜「const app =」;:799,搜「new Map()」)。 ↩ -
出处:「Quick Start」第 794–795 段(
text/10-ch04-chapter-4-quick-start.txt:794,搜「only the ranges were configured」)。 ↩ -
出处:「Quick Start」第 850–862 段(
text/10-ch04-chapter-4-quick-start.txt:851,搜「d[app.current]」;:861,搜「sortData」)。 ↩ -
出处:「Quick Start」第 900–933 段(
text/10-ch04-chapter-4-quick-start.txt:908,搜「renderControls」;:912,搜「property("disabled"」)。按钮 HTML 原型见第 902–906 段。 ↩ -
出处:「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.」 ↩ -
出处:「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.」 ↩↩ -
出处:「Quick Start」第 1002–1006 段(
text/10-ch04-chapter-4-quick-start.txt:1006,搜「d3.max([d.avg, d.max, d.min])」)。 ↩ -
出处:「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.」 ↩ -
出处:「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」。 ↩ -
出处:「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()」)。 ↩ -
出处:「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.」 ↩↩ -
出处:「Quick Start」第 1055–1069 段(
text/10-ch04-chapter-4-quick-start.txt:1057,搜「single file」)。三条理由原文逐条列出(例子小好组织、模块封装方便控制台实验、避免加载失败干扰)。 ↩ -
出处:「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」)。 ↩