d3 堆叠图形完全指南:stack 生成器、内置 order 与 offset 的实现与实战
2026/9/7 16:28:26 网站建设 项目流程

d3 堆叠图形完全指南:stack 生成器、内置 order 与 offset 的实现与实战

【免费下载链接】d3Bring data to life with SVG, Canvas and HTML. :bar_chart::chart_with_upwards_trend::tada:项目地址: https://gitcode.com/GitHub_Trending/d3/d3

本文基于 d3 官方文档仓库中 stacks 文档 整理,系统讲解 d3-shape 提供的 stack 生成器:从d3.stack()的构造与调用、.keys()/.value()/.order()/.offset()四个访问器,到全部 6 种内置堆叠顺序与 5 种内置堆叠偏移的用法与适用场景,并结合本仓库的导出机制与文档渲染管线给出可验证的使用依据。读完后你可以独立完成堆叠条形图、面积堆叠图与 streamgraph(河流图)的数据计算与渲染。

什么是 Stack:把"长度"转换为连续的位置区间

Stacking(堆叠)的作用是把一组长度转换为连续的位置区间。例如,一张按月统计销售额的条形图可以按品类拆分为多序列条形图:条形在垂直方向上逐层堆叠,并叠加一个分类颜色编码。堆叠图能够同时展示总量与各品类的分量;但也因此更难跨品类比较——因为堆叠后只有最底层的序列是对齐的。所以应当慎重选择 stack order,必要时考虑 streamgraph 这类居中流式布局。

与 pie 生成器 一样,stack 生成器并不直接产出图形。它计算的是位置,你可以把这些位置传给 area 生成器 来渲染面积图,或者直接使用它们来定位矩形(堆叠条形图)。

关于版本与获取方式:本仓库是 d3 主包(v7.9.0,见 package.json),它在 src/index.js 中通过export * from "d3-shape"全量转发了 d3-shape(依赖版本^3.2.0,见 package.json)。因此d3.stackd3.stackOrderAppearanced3.stackOffsetWiggle等 API 都可以直接挂在全局d3命名空间下使用,无需单独安装子包。

stack() 与stack(data, ...arguments)

stack()

stack()以默认设置构造一个新的 stack 生成器:

const stack = d3.stack();

stack(data, ...arguments)

对给定的data数组生成一个 stack,返回一个表示各序列(series)的数组。任意附加arguments都会被透传给访问器函数,并随this对象一起传递。下面用一个"月度水果销售"的整表(tidy table)示例走完整流程。

完整示例:按月、按水果堆叠销售数据

原始数据如下(每月 × 每种水果一行):

datefruitsales
1/2015apples3840
1/2015bananas1920
1/2015cherries960
1/2015durians400
2/2015apples1600
2/2015bananas1440
2/2015cherries960
2/2015durians400
3/2015apples640
3/2015bananas960
3/2015cherries640
3/2015durians400
4/2015apples320
4/2015bananas480
4/2015cherries640
4/2015durians400

在 JavaScript 中可以表示为对象数组(例如从 CSV 解析而来):

const data = [ {date: new Date("2015-01-01"), fruit: "apples", sales: 3840}, {date: new Date("2015-01-01"), fruit: "bananas", sales: 1920}, {date: new Date("2015-01-01"), fruit: "cherries", sales: 960}, {date: new Date("2015-01-01"), fruit: "durians", sales: 400}, {date: new Date("2015-02-01"), fruit: "apples", sales: 1600}, {date: new Date("2015-02-01"), fruit: "bananas", sales: 1440}, {date: new Date("2015-02-01"), fruit: "cherries", sales: 960}, {date: new Date("2015-02-01"), fruit: "durians", sales: 400}, {date: new Date("2015-03-01"), fruit: "apples", sales: 640}, {date: new Date("2015-03-01"), fruit: "bananas", sales: 960}, {date: new Date("2015-03-01"), fruit: "cherries", sales: 640}, {date: new Date("2015-03-01"), fruit: "durians", sales: 400}, {date: new Date("2015-04-01"), fruit: "apples", sales: 320}, {date: new Date("2015-04-01"), fruit: "bananas", sales: 480}, {date: new Date("2015-04-01"), fruit: "cherries", sales: 640}, {date: new Date("2015-04-01"), fruit: "durians", sales: 400} ];

要计算堆叠序列(每种fruit一个序列/层,每个date一个堆叠/列),可以:按datefruit对数据建立 index,用 union 求出fruit的去重取值,最后对每个datefruit取出sales值:

const series = d3.stack() .keys(d3.union(data.map(d => d.fruit))) // apples, bananas, cherries, … .value(([, group], key) => group.get(key).sales) (d3.index(data, d => d.date, d => d.fruit));

得到的数组每个元素对应一个series;每个 series 每月一个点,每个点由下界(lower,基线)与上界(upper,顶线)两个值定义:

[ [[ 0, 3840], [ 0, 1600], [ 0, 640], [ 0, 320]], // apples [[3840, 5760], [1600, 3040], [ 640, 1600], [ 320, 800]], // bananas [[5760, 6720], [3040, 4000], [1600, 2240], [ 800, 1440]], // cherries [[6720, 7120], [4000, 4400], [2240, 2640], [1440, 1840]] // durians ]

每个 series 通常再传给 area 生成器 渲染面积图,或用于构造条形图的矩形:

svg.append("g") .selectAll("g") .data(series) .join("g") .attr("fill", d => color(d.key)) .selectAll("rect") .data(D => D) .join("rect") .attr("x", d => x(d.data[0])) .attr("y", d => y(d[1])) .attr("height", d => y(d[0]) - y(d[1])) .attr("width", x.bandwidth());

返回结构的对应关系(重点):

  • 序列由 keys 访问器 决定;返回数组中第i个 series 对应第i个 key;
  • 每个 series 是一个点的数组,第j个点对应输入data中的第j个元素;
  • 每个点表示为[*y0*, *y1*]:y0为下界(基线),y1为上界(顶线),两者之差即该点由 value 访问器 计算出的值;
  • 每个 series 的 key 可通过series.key读取,堆叠顺序索引可通过series.index读取;
  • 每个点对应的输入数据元素可通过point.data读取。

stack.keys(keys)

若指定keys,则将 keys 访问器设置为给定的函数或数组,并返回该 stack 生成器:

const stack = d3.stack().keys(["apples", "bananas", "cherries", "durians"]);

若未指定keys,返回当前 keys 访问器:

stack.keys() // () => ["apples", "bananas", "cherries", "durians"]

keys 访问器默认是空数组;每个 key 会生成一个序列(层)。key 通常是字符串,但可以是任意值(参见 InternMap)。序列的 key 会连同每个数据点一起传给 value 访问器 以计算点的值。

stack.value(value)

若指定value,则将 value 访问器设置为给定的函数或数字,并返回该 stack 生成器:

const stack = d3.stack().value((d, key) => d[key]);

若未指定value,返回当前 value 访问器:

stack.value() // (d, key) => d[key]

value 访问器的默认实现是:

function value(d, key) { return d[key]; }

注意(文档原文警告):默认 value 访问器假设输入数据是暴露具名数值属性的对象数组,即"宽表"(wide)而非"整表"(tidy)形式,官方已不再推荐这种数据形态,建议参考上文 tidy 数据的示例。

stack.order(order)

若指定order,则将 order 访问器设置为给定的函数或数组,并返回该 stack 生成器:

const stack = d3.stack().order(d3.stackOrderNone);

order是函数,它会接收生成的 series 数组,并必须返回一个数值索引数组来表示堆叠顺序。例如按 key 的逆序堆叠:

const stack = d3.stack().order(series => d3.range(series.length).reverse());

两个时序细节很重要:

  1. 堆叠顺序先于 offset 计算;在计算 order 时,所有点的下界值均为零;
  2. 每个 series 的index属性也是在 order 计算之后才被设置。

若未指定order,返回当前 order 访问器:

stack.order() // d3.stackOrderNone

order 访问器默认是 stackOrderNone,即保持 keys 访问器 给出的顺序。内置顺序见下文。

stack.offset(offset)

若指定offset,则将 offset 访问器设置为给定的函数,并返回该 stack 生成器:

const stack = d3.stack().offset(d3.stackOffsetExpand);

offset 函数会接收生成的 series 数组与 order 索引数组,由它负责更新 series 数组中各点的下界与上界值。可以参考内置 offset 的实现作为范例。

若未指定offset,返回当前 offset 访问器:

stack.offset() // d3.stackOffsetExpand

offset 访问器默认是 stackOffsetNone,即使用零基线。内置偏移见下文。

内置堆叠顺序(Stack orders)

堆叠顺序通常不直接使用,而是传给stack.order。

stackOrderAppearance(series)

const stack = d3.stack().order(d3.stackOrderAppearance);

返回一种序列顺序:按最大值出现得越早的序列越靠底部(earliest series, according to the maximum value, is at the bottom)。适合"哪个品类先起量就压在最下边"的叙事。

stackOrderAscending(series)

const stack = d3.stack().order(d3.stackOrderAscending);

返回一种序列顺序:按取值之和最小的序列在最底部(smallest series, according to the sum of values, is at the bottom)。

stackOrderDescending(series)

const stack = d3.stack().order(d3.stackOrderDescending);

返回一种序列顺序:按取值之和最大的序列在最底部(largest series, according to the sum of values, is at the bottom)。这是常见的"大头在下"布局,可显著提升底部对齐层的可比性。

stackOrderInsideOut(series)

const stack = d3.stack().order(d3.stackOrderInsideOut);

返回一种序列顺序:按最大值出现得越早的序列在内侧,较晚的序列在外侧。该顺序推荐与 wiggle 偏移 搭配用于 streamgraph。其理论依据来自 Byron 与 Wattenberg 的论文《Stacked Graphs — Geometry & Aesthetics》。

stackOrderNone(series)

const stack = d3.stack().order(d3.stackOrderNone);

返回给定的序列顺序[0, 1, … *n* - 1](n 为series的元素个数),即堆叠顺序直接由 keys 访问器 决定。这是默认顺序。

stackOrderReverse(series)

const stack = d3.stack().order(d3.stackOrderReverse);

返回给定序列顺序的逆序[*n* - 1, *n* - 2, … 0],即堆叠顺序为 keys 访问器 的逆序

内置堆叠偏移(Stack offsets)

堆叠偏移同样不直接使用,而是传给stack.offset。

stackOffsetExpand(series,order)

const stack = d3.stack().offset(d3.stackOffsetExpand);

应用零基线,并对每个点的值做归一化,使顶线恒为 1。效果是每列总量都占满 100%,适合展示"构成比例"而非绝对量。

stackOffsetDiverging(series,order)

const stack = d3.stack().offset(d3.stackOffsetDiverging);

正值堆叠在零之上,负值堆叠在零之下,零值堆叠在零线位置。适合带正负分量的发散堆叠图(如收支、增减量对比)。

stackOffsetNone(series,order)

const stack = d3.stack().offset(d3.stackOffsetNone);

应用零基线,即普通自底向上的堆叠。这是默认偏移。

stackOffsetSilhouette(series,order)

const stack = d3.stack().offset(d3.stackOffsetSilhouette);

将基线下移,使streamgraph 的中心线始终位于零轴。每列的总高度仍保持为各分量之和,但整体围绕零轴上下对称分布。

stackOffsetWiggle(series,order)

const stack = d3.stack().offset(d3.stackOffsetWiggle);

移动基线以最小化各层的加权"抖动"(weighted wiggle)。该偏移推荐与 inside-out 顺序 搭配用于 streamgraph,依据同样是 Byron 与 Wattenberg 的《Stacked Graphs — Geometry & Aesthetics》。

下表汇总各内置实现的定位,便于选型:

类别名称行为摘要文档标注的源文件(位于 d3-shape 仓库)
orderstackOrderAppearance最大值最早出现的序列在底部src/order/appearance.js
orderstackOrderAscending求和最小的序列在底部src/order/ascending.js
orderstackOrderDescending求和最大的序列在底部src/order/descending.js
orderstackOrderInsideOut早出现的在内侧,晚出现的外侧(streamgraph 推荐)src/order/insideOut.js
orderstackOrderNone保持 keys 顺序[0, 1, …, n-1](默认)src/order/none.js
orderstackOrderReversekeys 顺序的逆序src/order/reverse.js
offsetstackOffsetExpand零基线 + 每点归一化使顶线为 1src/offset/expand.js
offsetstackOffsetDiverging正值零上、负值零下、零值在零线src/offset/diverging.js
offsetstackOffsetNone零基线(默认)src/offset/none.js
offsetstackOffsetSilhouette基线下移使中心线恒为零src/offset/silhouette.js
offsetstackOffsetWiggle最小化加权抖动(streamgraph 推荐)src/offset/wiggle.js

在本仓库中验证与使用这些 API

以下几点可以帮助你在实际项目里核对与运行上述内容:

  • 命名空间完整性有测试保证:test/d3-test.js 会遍历 package.json 中声明的每一个依赖模块(含 d3-shape),断言该模块导出的每个属性都出现在d3命名空间中。这意味着文档中出现的所有d3.stack*API 均由主包直接提供。
  • 文档站本身就是演示环境:本仓库用 VitePress 构建文档(见 package.json 中的docs:dev/docs:build/docs:preview脚本与 prebuild.sh)。stack.md 顶部的<script setup>通过d3.csv(..., d3.autoType)加载 RIAA 唱片营收数据集(数据文件见 riaa-us-revenue.csv),并用 PlotRender 组件 渲染各 order/offset 的实时对比图。运行yarn docs:dev即可在浏览器中看到每种顺序与偏移在真实数据上的差异。
  • CDN/全局用法:bundle.js 是 UMD 入口,转发version与 src/index.js 的全部导出,适合通过<script>引入整包 d3 的场景;ES 模块场景则直接使用src/index.js作为入口(package.json 的module/exports配置)。
  • 数据管线衔接:整表数据可先用 d3-dsv 的d3.csv+d3.autoType解析,再配合 d3-array 的indexunion完成"分组 → 取 keys → 取值"三步,这正是官方示例的推荐路径。

实战选型建议(基于上述文档行为总结)

  • 堆叠条形图/普通面积堆叠:stackOrderDescending+stackOffsetNone(默认偏移),让最大序列贴底、便于跨列比较;
  • 构成比例图:stackOffsetExpand,纵轴按百分比解释;
  • 正负分量对比:stackOffsetDiverging,让零轴成为分界线;
  • streamgraph(河流图):stackOrderInsideOut+stackOffsetWiggle,必要时用stackOffsetSilhouette保证中心对齐零轴;
  • 需要固定业务顺序(如年份、等级)时保持默认的stackOrderNone并用reverse: true类逆序需求改用stackOrderReverse

stack 生成器的本质是"数据整形器":它不关心像素与路径,只负责把 tidy 数据整理为带keyindexdata[y0, y1]区间的序列数组,再由 area 生成器或矩形绑定完成绘制。理解 order 与 offset 的计算先后顺序(先 order 后 offset、offset 阶段下界恒为零)以及返回结构与输入数据的索引对应关系,是正确编写后续渲染与交互代码的关键。

【免费下载链接】d3Bring data to life with SVG, Canvas and HTML. :bar_chart::chart_with_upwards_trend::tada:项目地址: https://gitcode.com/GitHub_Trending/d3/d3

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询