D3·7.9 概述
D3(Data-Driven Documents)官方定位:**The JavaScript library for bespoke data visualization**——用于定制数据可视化的 JavaScript 库,以无与伦比的灵活性创建自定义动态可视化。 v7.9.0(2023 年发布)是 7.x 系列的最新稳定版,相对早期版本的主要变化: - **ESM 优先**:官方推荐以 ES 模块方式引入;同时仍提供 UMD 包(`dist/d3.min.js`),普通 script 标签加载后得到全局 `d3` - **模块化**:d3 是 30 余个子模块(d3-selection、d3-scale、d3-shape、d3-force……)的合集,可整包引入也可按需安装 - **API 现代化**:`d3.csv/d3.json` 等 fetch 系列返回 Promise;数据连接新增 `selection.join()` 简写 enter/update/exit - **时间处理推荐 UTC**:`d3.scaleUtc` 替代本地时区的 `d3.scaleTime`,行为可预期 本页示例均来自 d3-7.9.0 自带 `docs/` 目录的官方文档示例(已适配为可直接运行的写法),页面内嵌的演示由本站点内置的 `d3.790.js`(v7.9.0 UMD)驱动。
官网与下载: ```text 官网 https://d3js.org/ 历史版本下载(换成想要的版本号) https://registry.npmjs.org/d3/-/d3-7.9.0.tgz ``` 本页直接引入站点内置的 7.9.0 UMD 包: ```html <script type="text/javascript" src="/static/js/d3.790.js"></script> ``` npm / yarn 方式: ```bash npm i d3@7.9.0 # 或 yarn add d3@7.9.0 ``` ```js import * as d3 from "d3"; // 整包 import { select, scaleLinear } from "d3"; // 按需 import { mean, median } from "d3-array"; // 子包 ```
官方推荐 ESM + CDN 方式(`type="module"` 中 import): ```html <div id="container"></div> <script type="module"> import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm"; // ……此处写 d3 代码…… </script> ``` 传统 UMD 方式(普通 script 标签,得到全局 `d3`): ```html <div id="container"></div> <script src="https://cdn.jsdelivr.net/npm/d3@7"></script> <script> // ……此处写 d3 代码,直接使用全局 d3…… </script> ``` 二者 API 完全一致;本页演示采用 UMD 全局 `d3` 写法。
第一个图表:坐标轴骨架
出处:docs/getting-started.md。一张图的骨架 = 边距约定 + 两个比例尺 + 两个 `.call(axis)`,六步完成: ```js // 1. 声明图表尺寸与边距(D3 经典边距约定) const width = 640, height = 400; const marginTop = 20, marginRight = 20, marginBottom = 30, marginLeft = 40; // 2. 声明 x(水平方向)比例尺:UTC 时间 const x = d3.scaleUtc() .domain([new Date("2023-01-01"), new Date("2024-01-01")]) .range([marginLeft, width - marginRight]); // 3. 声明 y(垂直方向)比例尺:线性 const y = d3.scaleLinear() .domain([0, 100]) .range([height - marginBottom, marginTop]); // 4. 创建 SVG 容器 const svg = d3.select("#container").append("svg") .attr("width", width) .attr("height", height); // 5. 添加 x 轴 svg.append("g") .attr("transform", `translate(0,${height - marginBottom})`) .call(d3.axisBottom(x)); // 6. 添加 y 轴 svg.append("g") .attr("transform", `translate(${marginLeft},0)`) .call(d3.axisLeft(y)); ``` 要点:`scaleUtc` 处理时间、`scaleLinear` 处理数值;`append("g")` 分组后用 `call` 把坐标轴生成器应用到分组上。
数据绑定 join
出处:docs/d3-selection/joining.md。嵌套 data join——外层绑定行数组,内层用 `d => d` 把每行数组再绑定到 td;`.join("tr")` 是 enter+update+exit 的简写: ```js const matrix = [ [11975, 5871, 8916, 2868], [ 1951, 10048, 2060, 6171], [ 8010, 16145, 8090, 8045], [ 1013, 990, 940, 6907] ]; d3.select("body") .append("table") .selectAll("tr") .data(matrix) .join("tr") .selectAll("td") .data(d => d) .join("td") .text(d => d); ```
`enter().append()` 创建缺失元素,`exit().remove()` 移除多余元素;给 `data()` 传 key 函数(`d => d`)可按值追踪元素。点击上面按钮观察增删效果。 ```js const div = d3.select("body") .selectAll("div") .data([4, 8, 15, 16, 23, 42]) .enter().append("div") .text(d => d); // 更新:用 key 函数重绑后 // div = div.data([1, 2, 4, 8, 16, 32], d => d); // div.enter().append("div").text(d => d); // div.exit().remove(); ``` 链式缩进约定(docs/d3-selection/selecting.md):返回**新**选择集的方法缩进两格(如 selectAll/data/enter),返回**原**选择集的方法缩进四格(如 attr/text)。
比例尺 scale
出处:docs/d3-scale/linear.md。比例尺就是"定义域 domain → 值域 range"的映射函数;数值值域可用 `invert` 反查(交互拾取场景)。 ```js const x = d3.scaleLinear([10, 130], [0, 960]); x(20); // 80 x(50); // 320 x.invert(80); // 20 // 值域也可以是颜色——线性插值混色: d3.scaleLinear([10, 100], ["brown", "steelblue"])(50); ```
出处:docs/d3-scale/band.md。柱状图标配——`x(d.category)` 给柱子左缘,`x.bandwidth()` 给柱宽,`paddingInner/paddingOuter` 控制间隙比例: ```js const x = d3.scaleBand(["a", "b", "c"], [0, 960]).paddingInner(0.1); x("a"); // 0 x("b"); // 320 x("c"); // 640 x("d"); // undefined(domain 外的值) x.bandwidth(); // 每个档位的宽度(画柱子的宽) x.step(); // 档距 = bandwidth + 间距 ```
出处:docs/d3-scale/time.md。`scaleTime` 用本地时区、`scaleUtc` 用 UTC(官方推荐 UTC,行为可预期);`ticks` 自动产生"整点"等可读刻度,也可按时间间隔定制。 ```js const x = d3.scaleUtc([new Date("2000-01-01"), new Date("2000-01-02")], [0, 960]); x(new Date("2000-01-01T05:00Z")); // 200 x.invert(200); // 2000-01-01T05:00Z x.ticks(10); // 自动产生可读刻度 x.ticks(d3.utcMinute.every(15)); // 每 15 分钟一个刻度 ```
出处:docs/d3-scale/ordinal.md。离散 → 离散映射,常用于色板;不显式设 domain 时会"隐式推断"(官方警示:建议显式 domain 保证确定性);`unknown` 设置未知值兜底。 ```js const color = d3.scaleOrdinal(["a", "b", "c"], ["red", "green", "blue"]); color("a"); // "red" // 用内置色板 + 未知值兜底: const color2 = d3.scaleOrdinal(["a", "b", "c"], d3.schemeTableau10).unknown(null); color2("d"); // null ```
坐标轴 axis
出处:docs/d3-axis.md。四种朝向:`axisTop / axisRight / axisBottom / axisLeft(scale)`——名称指刻度文字的朝向,轴线起点默认在 `[0,0]`,用 `transform` 平移到位后 `call` 渲染: ```js const gx = svg.append("g") .attr("transform", `translate(0,${height - marginBottom})`) .call(d3.axisBottom(x)); // 比例尺变化后可平滑更新(详见右侧演示): gx.transition() .duration(750) .call(d3.axisBottom(x)); ```
出处:docs/d3-axis.md。`ticks(数量, 格式)` 把参数传给比例尺的 ticks/tickFormat;`"s"` 为 SI 前缀格式(10k 等);时间轴可传 `d3.timeMinute.every(15)`。 ```js axis.ticks(20, "s"); // 约 20 个刻度,SI 前缀格式 axis.ticks(d3.timeMinute.every(15)); // 时间轴每 15 分钟一个刻度 axis.tickFormat(d3.format(".0%")); // 百分比格式 axis.tickValues([1, 3, 5, 9]); // 指定刻度值 ```
柱状图 bar
出处:docs/d3-scale/band.md、docs/d3-axis.md(经典字母频率柱状图)。核心要点: - `scaleBand` 负责 x:domain 是字母数组,`x(d.letter)` 给左缘、`x.bandwidth()` 给柱宽、`padding(0.1)` 设间隙 - `scaleLinear` 负责 y:domain 到 `d3.max`,`.nice()` 把刻度取整更美观 - y 轴 `ticks(10, "%")` 以百分比格式化刻度 ```js const x = d3.scaleBand() .domain(data.map(d => d.letter)) .range([marginLeft, width - marginRight]) .padding(0.1); const y = d3.scaleLinear() .domain([0, d3.max(data, d => d.frequency)]).nice() .range([height - marginBottom, marginTop]); svg.append("g") .attr("fill", "steelblue") .selectAll("rect") .data(data) .join("rect") .attr("x", d => x(d.letter)) .attr("y", d => y(d.frequency)) .attr("height", d => y(0) - y(d.frequency)) .attr("width", x.bandwidth()); svg.append("g") .attr("transform", `translate(0,${height - marginBottom})`) .call(d3.axisBottom(x)); svg.append("g") .attr("transform", `translate(${marginLeft},0)`) .call(d3.axisLeft(y).ticks(10, "%")); ```
折线与面积 line·area
出处:docs/d3-shape/line.md、area.md。`d3.line()` / `d3.area()` 返回的**生成器**把数据数组转成 path 的 `d` 字符串: ```js const line = d3.line() .x((d) => x(d.Date)) .y((d) => y(d.Close)); svg.append("path") .attr("d", line(data)) .attr("stroke", "currentColor") .attr("fill", "none"); ``` ```js const area = d3.area() .x((d) => x(d.Date)) .y0(y(0)) // 基线:通常固定为零线 .y1((d) => y(d.Close)); // 顶线 svg.append("path").attr("d", area(data)); ``` 要点:面积 = 顶线 `(x, y1)` + 基线 `(x0, y0)`,水平面积图只需 `x / y0 / y1` 三个访问器;折线可配 `.defined(...)` 处理缺失数据、`.curve(d3.curveStep)` 更换插值方式。
饼图与圆环 pie·arc
出处:docs/d3-shape/pie.md、arc.md。分工:**pie 只算角度不画图**,**arc 把角度对象转成 path**;弧以原点为圆心,靠 transform 移到画布中心。 ```js const data = [1, 1, 2, 3, 5, 8, 13, 21]; const pie = d3.pie(); // 对象数据时用: d3.pie().value((d) => d.value) const arcs = pie(data); // 每项含 {data, value, index, startAngle, endAngle, padAngle} ``` ```js const arc = d3.arc() .innerRadius(80) // 内半径 > 0 即成圆环;为 0 是饼图 .outerRadius(180) .cornerRadius(8) // 扇形圆角 .padAngle(0.03); // 扇区间隙 // arc.centroid(d) 返回分片中心点,常用于定位标签 ``` ```js // 也可以一次性传角度对象给 arc(): svg.append("path") .attr("transform", "translate(100,100)") .attr("d", d3.arc()({ innerRadius: 100, outerRadius: 200, startAngle: -Math.PI / 2, endAngle: Math.PI / 2 })); ```
过渡动画 transition
出处:docs/d3-transition.md(按官方 API 组合的可运行示例)。绿色 = enter 进入、蓝色 = update 更新、红色 = exit 退出。join 的 enter/update/exit 各自接 transition 即完成三态动画: ```js svg.selectAll("rect") .data(data, d => d.name) // key:按名字追踪 .join( enter => enter.append("rect") /* …初始属性… */ .call(enter => enter.transition().duration(750) .attr("y", d => y(d.value)) .attr("height", d => y(0) - y(d.value))), update => update .call(update => update.transition().duration(750) .attr("y", d => y(d.value)) .attr("height", d => y(0) - y(d.value))), exit => exit .call(exit => exit.transition().duration(750) .attr("height", 0).attr("y", y(0)) .remove()) ); ```
出处:docs/d3-transition.md、timing.md。最小过渡: ```js d3.select("body") .transition() .style("background-color", "red"); ``` 时序参数: ```js transition.delay((d, i) => i * 10); // 按索引错峰(交错动画) transition.duration(750); // 毫秒 transition.ease(d3.easeCubic); // 缓动函数,默认 easeCubic ``` 常用缓动家族(docs/d3-ease.md),每个家族都有 In / Out / InOut 三变体(如 `d3.easeCubicInOut`): | 名称 | 效果 | | --- | --- | | easeLinear | 线性,速度不变 | | easePoly | 多项式(可设 exponent) | | easeQuad / easeCubic | 二次 / 三次(默认) | | easeSin / easeExp | 正弦 / 指数 | | easeCircle | 圆形加速进入 | | easeElastic | 有弹性(可设 amplitude/period) | | easeBack | 先回退再到位(可设 overshoot) | | easeBounce | 像皮球落地反复弹跳 |
交互 zoom·drag
在图表区域滚轮缩放、按住拖动平移;橙色圆点可拖拽
出处:docs/d3-zoom.md。zoom 状态存在元素上;`event.transform`(含 k/x/y)直接当 transform 用,或用 `rescaleX/rescaleY` 重建比例尺驱动坐标轴。 ```js const zoom = d3.zoom() .scaleExtent([1, 8]) // 缩放范围 .on("zoom", zoomed); svg.call(zoom); function zoomed(event) { g.attr("transform", event.transform); // 内容组整体变换 // 或重映射比例尺画轴: // gx.call(xAxis.scale(event.transform.rescaleX(x))); } // 程序化复位: svg.transition().duration(750).call(zoom.transform, d3.zoomIdentity); ```
出处:docs/d3-drag.md。拖拽三事件 start / drag / end;`event.x/y` 是相对 container 的指针坐标,写回属性即可驱动渲染(左侧演示中的橙色圆点即此模式): ```js d3.selectAll("circle.node") .call(d3.drag() .on("start", function (event) { d3.select(this).raise(); // 提到最上层 }) .on("drag", function (event, d) { d.x = event.x; d.y = event.y; // 写回 datum d3.select(this).attr("cx", d.x).attr("cy", d.y); })); ```
力导向图 force
出处:docs/d3-force/simulation.md、many-body.md、link.md、collide.md。注册四种力: ```js const simulation = d3.forceSimulation(nodes) .force("charge", d3.forceManyBody().strength(-100)) // 排斥力(负值) .force("link", d3.forceLink(links)) // 链接按 source/target 找节点 .force("collide", d3.forceCollide((d) => d.r)) // 碰撞力 .force("center", d3.forceCenter(width / 2, height / 2)); ``` 模拟器启动后每个 tick 自动运行;渲染方只需在 tick 回调里把 `node.x/y` 写回 DOM;`forceLink(links).id(d => d.id)` 指定用哪个字段解析链接端点: ```js simulation.on("tick", () => { link.attr("x1", d => d.source.x).attr("y1", d => d.source.y) .attr("x2", d => d.target.x).attr("y2", d => d.target.y); node.attr("cx", d => d.x).attr("cy", d => d.y); }); ``` 注意(官方警告):`forceSimulation` 是 impure 的——它会**直接改写**传入的 nodes 数组,给每个节点添加 `x/y/vx/vy` 属性。
层次布局 tree
出处:docs/d3-hierarchy/hierarchy.md、tree.md。三步:包装 → 布局 → 渲染: ```js // ① d3.hierarchy(data) 把嵌套 JSON 包装成节点树 const root = d3.hierarchy(data); // ② d3.tree().size([w, h])(root) 做 Reingold–Tilford 布局,之后每个 node 有 x, y d3.tree().size([600, 360])(root); // ③ root.links() 给连线数据,root.descendants() 给节点数据 g.selectAll("line").data(root.links()).join("line") /* … */; g.selectAll("circle").data(root.descendants()).join("circle") /* … */; ``` 同族布局(docs/d3-hierarchy/ 目录): - `d3.cluster`:所有叶子对齐到同一深度的树 - `d3.treemap`:矩形树图,需先 `root.sum(...)`,布局后得 `x0/y0/x1/y1` 矩形 - `d3.pack`:圆形嵌套打包图;`d3.partition`:分区图(常用于旭日图)
数据加载与工具
出处:docs/d3-fetch.md、d3-dsv.md。fetch 系列返回 Promise;CSV 字段默认都是字符串,行转换函数(或 `d3.autoType`)负责类型化,返回 null/undefined 可过滤行。上面演示加载本站 `/static/js/data/cars.csv` 绘制柱状图。 ```js const data = await d3.csv("example.csv"); // [{"Hello": "world"}, …] const json = await d3.json("example.json"); // 行转换函数: const rows = await d3.dsv(",", "example.csv", (d) => { return { year: new Date(+d.Year, 0, 1), // "Year" 列 → Date make: d.Make, model: d.Model, length: +d.Length // "Length" 列 → number }; }); // 自动类型推断(演示所用): d3.csv("example.csv", d3.autoType); ```
出处:docs/d3-array/(summarize.md、group.md、bin.md)。最常用的聚合与分组工具: ```js d3.extent([3, 2, 1, 1, 6, 2, 4]); // [1, 6](最小/最大值一次拿到) d3.extent(alphabet, (d) => d.frequency); // [0.00074, 0.12702] // rollup:分组 + 聚合 → InternMap const speciesCount = d3.rollup(penguins, (D) => D.length, (d) => d.species); speciesCount.get("Adelie"); // 152 // rollups:数组版;group:只分组不聚合 d3.rollups(penguins, (D) => D.length, (d) => d.species); // [["Adelie", 152], …] d3.group(penguins, (d) => d.species); // 键 → 原行数组 // bin:直方图分箱,每个 bin 附带 x0/x1 区间端点,可直接画直方图 const bins = d3.bin().value((d) => d.culmen_length_mm)(penguins); ```
出处:docs/d3-format.md、d3-time-format.md。 ```js // d3-format:数值格式化(坐标轴 tickFormat 常配) const f = d3.format(".1f"); f(0.1 * 3); // "0.3" d3.format(".0%")(0.123); // "12%" d3.format("($.2f")(-3.5); // 本地化货币 // d3-time-format:Date ↔ 字符串 const format = d3.timeFormat("%b %d"); // Date → "Mar 29" const parse = d3.timeParse("%b %d"); // 字符串 → Date d3.isoParse("2023-05-31T18:17:36.788Z"); // ISO 字符串 → Date ```
出处:docs/d3-scale-chromatic.md。三类内置色带: - **分类色板**:`schemeCategory10`、`schemeObservable10`、`schemeTableau10`、`schemeAccent`、`schemeDark2`、`schemePaired`、`schemePastel1/2`、`schemeSet1/2/3` - **顺序色(连续插值)**:`interpolateViridis`、`interpolateInferno`、`interpolateMagma`、`interpolatePlasma`、`interpolateCividis`、`interpolateTurbo`、`interpolateRainbow`,及 ColorBrewer 单/多色系 `interpolateBlues/Greens/Oranges/Purples/Reds/BuGn/GnBu/OrRd/PuBu/YlGn/YlOrBr` 等(均有 `scheme*` 离散数组版,如 `schemeBlues[5]`) - **发散色**:`interpolateBrBG/PRGn/PiYG/PuOr/RdBu/RdGy/RdYlBu/RdYlGn/Spectral`(同名 `scheme*` 版) 用法: ```js // 分类:直接作为 scaleOrdinal 的 range const color = d3.scaleOrdinal(d3.schemeAccent); // 连续:配合 scaleSequential,用 domain 映射数值到颜色 const heat = d3.scaleSequential(d3.interpolateViridis).domain([0, 100]); heat(50); // 中间色 ```
参考
    D3 官网(v7.9 文档即本页示例出处)
    D3 Gallery(官方图表示例库)