d3 堆叠图形完全指南:stack 生成器、内置 order 与 offset 的实现与实战
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.stack、d3.stackOrderAppearance、d3.stackOffsetWiggle等 API 都可以直接挂在全局d3命名空间下使用,无需单独安装子包。stack() 与stack(data, ...arguments)stack()stack()以默认设置构造一个新的 stack 生成器:const stack d3.stack();stack(data, ...arguments)对给定的data数组生成一个 stack,返回一个表示各序列(series)的数组。任意附加arguments都会被透传给访问器函数,并随this对象一起传递。下面用一个月度水果销售的整表(tidy table)示例走完整流程。完整示例:按月、按水果堆叠销售数据原始数据如下(每月 × 每种水果一行):datefruitsales1/2015apples38401/2015bananas19201/2015cherries9601/2015durians4002/2015apples16002/2015bananas14402/2015cherries9602/2015durians4003/2015apples6403/2015bananas9603/2015cherries6403/2015durians4004/2015apples3204/2015bananas4804/2015cherries6404/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一个堆叠/列),可以:按date与fruit对数据建立 index,用 union 求出fruit的去重取值,最后对每个date、fruit取出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());两个时序细节很重要:堆叠顺序先于 offset 计算;在计算 order 时,所有点的下界值均为零;每个 series 的index属性也是在 order 计算之后才被设置。若未指定order,返回当前 order 访问器:stack.order() // d3.stackOrderNoneorder 访问器默认是 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.stackOffsetExpandoffset 访问器默认是 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.jsorderstackOrderAscending求和最小的序列在底部src/order/ascending.jsorderstackOrderDescending求和最大的序列在底部src/order/descending.jsorderstackOrderInsideOut早出现的在内侧,晚出现的外侧(streamgraph 推荐)src/order/insideOut.jsorderstackOrderNone保持 keys 顺序[0, 1, …, n-1](默认)src/order/none.jsorderstackOrderReversekeys 顺序的逆序src/order/reverse.jsoffsetstackOffsetExpand零基线 每点归一化使顶线为 1src/offset/expand.jsoffsetstackOffsetDiverging正值零上、负值零下、零值在零线src/offset/diverging.jsoffsetstackOffsetNone零基线(默认)src/offset/none.jsoffsetstackOffsetSilhouette基线下移使中心线恒为零src/offset/silhouette.jsoffsetstackOffsetWiggle最小化加权抖动(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.csvd3.autoType解析,再配合 d3-array 的index与union完成分组 → 取 keys → 取值三步,这正是官方示例的推荐路径。实战选型建议(基于上述文档行为总结)堆叠条形图/普通面积堆叠:stackOrderDescendingstackOffsetNone(默认偏移),让最大序列贴底、便于跨列比较;构成比例图:stackOffsetExpand,纵轴按百分比解释;正负分量对比:stackOffsetDiverging,让零轴成为分界线;streamgraph(河流图):stackOrderInsideOutstackOffsetWiggle,必要时用stackOffsetSilhouette保证中心对齐零轴;需要固定业务顺序(如年份、等级)时保持默认的stackOrderNone并用reverse: true类逆序需求改用stackOrderReverse。stack 生成器的本质是数据整形器:它不关心像素与路径,只负责把 tidy 数据整理为带key、index、data与[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),仅供参考