D3(v7)入门七:堆叠数据与柱状图

3,185 阅读5分钟

堆叠图是柱状图的一种形式,它显示了一些相对或绝对变量随时间变化的构成和比较。看起来像一系列堆叠在一起的柱状图。如果使用得当,堆叠图是一种非常有效的比较工具。它们旨在比较不同类别的总值。点击查看Demo

image.png

d3.stack

D3提供了处理生成堆叠数据的API——d3.stack:

  • d3.stack - 创建一个新的堆叠生成器。
  • stack - 为给定数据生成堆叠数据。
  • stack.keys - 设置键访问器。
  • stack.value - 设置值访问器。
  • stack.order - 设置排序访问器。
    • d3.stackOrderAscending - 将最小值放在底部。
    • d3.stackOrderDescending - 将最大值放在底部。
    • d3.stackOrderInsideOut - 将最大值放在中部。
    • d3.stackOrderNone - 使用给定的系列顺序。
    • d3.stackOrderReverse - 系列给定的系列相反的顺序。
  • stack.offset - 设置偏移访问器。
    • d3.stackOffsetExpand - 标准化为0=1之间。
    • d3.stackOffsetNone - 应用零基准。
    • d3.stackOffsetSilhouette - 将流图居中在0附近。
    • d3.stackOffsetWiggle - 流图最小摆动。
const data = [
  {month: new Date(2015, 0, 1), apples: 3840, bananas: 1920, cherries: 960, dates: 400},
  {month: new Date(2015, 1, 1), apples: 1600, bananas: 1440, cherries: 960, dates: 400},
  {month: new Date(2015, 2, 1), apples:  640, bananas:  960, cherries: 640, dates: 400},
  {month: new Date(2015, 3, 1), apples:  320, bananas:  480, cherries: 640, dates: 400}
];

const stack = d3.stack()
    .keys(["apples", "bananas", "cherries", "dates"])
    .order(d3.stackOrderNone) // 设置排序访问器 使用给定的系列顺序
    .offset(d3.stackOffsetNone); // 设置偏移访问器 应用零基准

const dataset = stack(data);
console.log(dataset);

我们会得到如下的一个数组:

[
    [[   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]], // dates
]

最开始,我理解的生成的数组,对应的就是每一个时间对应的柱状图,类似下面这种格式:

[
    [[apples.st, apples.ed], [bananas.st, bananas.ed], [cherries.st, cherries.ed], [dates.st, dates.ed]]
    ...
]

实际情况却是,把需要堆叠的数据按顺序处理为一组,每组对应数组下标的数据,组成一个柱形图:

image.png

感觉说的很绕口,这样我们只先展示apple的数据:

const stack = d3.stack()
    .keys(["apples"])
    
/*   
[
  [[0, 3840], [0, 1600], [0,  640], [0,  320]]
]
*/

这样就是生成四个柱形图,每个柱形图只有apple这一个类别的数据。 image.png

其实这里就可以联想到Echarts中的图例(legend)是怎么控制不同类别的数据是否显示的,在D3的堆叠数据中来说,我们只需要控制keys的值就行了,很方便。

堆叠柱状图

比例尺

我们需要三个比例尺:x轴比例尺,y轴比例尺和颜色比例尺。关于比例尺的具体内容,本章就不再重复了,大家可以回看D3(v7)入门四:比例尺。

const xScale = d3.scaleBand()
    .domain(data.map((d) => d.month))
    .range([0, dimensions.boundedWidth])
    .padding(0.1);

const yScale = d3.scaleLinear()
    .domain([0, d3.max(dataset, (d) => d3.max(d, (d) => d[1]))])
    .range([dimensions.boundedHeight, 0]);

const color = d3.scaleOrdinal()
    .domain(["apples", "bananas", "cherries", "dates"])
    .range(["#c51b7d", "#e9a3c9", "#fde0ef", "#f1e4f1"]);

绘制数据

bounds.selectAll("g")
.data(dataset) // 绑定数据绘制四个分组(柱状图盒子)
.join("g")
.selectAll("rect")
.data((d) => d) // 绑定每一组的数据,绘制四个矩形
.join("rect")
.attr("x", (d, i) => {
   console.log(d)
   return xScale(d.data.month)
})
.attr("y", (d) => yScale(d[1]))
.attr("width", xScale.bandwidth())
.attr("height", (d) => yScale(d[0]) - yScale(d[1]))
.attr("fill", (d, i, nodes) => {
    console.log(nodes[i].parentNode.__data__)
    return color(nodes[i].parentNode.__data__.key);
});

每一个d的数据: image.png

每一个分组g上绑定的数据nodes[i].parentNode.__data__: image.png

image.png

坐标轴

const xAxis =d3.axisBottom(xScale)
.tickFormat(d3.timeFormat("%b"));
const yAxis = d3.axisLeft(yScale);

bounds.append("g")
.attr("transform", `translate(0,${dimensions.boundedHeight})`).call(xAxis);
bounds.append("g").call(yAxis);

这里用到了tickFormatd3.timeFormattickFormat支持自定义刻度显示内容格式。

d3.timeFormat

const dateString = new Date()
const today = d3.timeFormat("%Y-%m-%d %H:%M:%S")(dateString)
console.log(today) // 2022-11-12 21:51:07
  • %f - milliseconds 毫秒
  • %S - seconds 秒
  • %M - minute 分钟
  • %H - hour (24h)
  • %I - hour (12h)
  • %p - AM / PM
  • %d - day (01-31)
  • %a - abbreviated 工作日缩写
  • %A - 工作日
  • %b - abbreviated 月份缩写
  • %B - 月份
  • %m - month (01-12)
  • %Y - year (four digits)
  • %Z - time zone offset 时区偏移百分比

还有一些高级的用法:

const start = new Date(2020, 0, 1)
const end = new Date(2022, 0, 2)
console.log(d3.timeDay.count(start, end)) // 计算两个日期之间的天数

console.log(d3.timeDay.every(2).range(start, end).map(d3.timeFormat("%Y-%m-%d"))) // 生成两个日期之间的每个第二天的日期数组
console.log(d3.timeFormat("%Y-%m-%d")(d3.timeDay.floor(start))) // 向下取整
console.log(d3.timeFormat("%Y-%m-%d")(d3.timeDay.ceil(start))) // 向上取整
console.log(d3.timeFormat("%Y-%m-%d")(d3.timeDay.round(start))) // 四舍五入
console.log(d3.timeFormat("%Y-%m-%d")(d3.timeDay.offset(start, 2))) // 日期偏移
console.log(d3.timeFormat("%Y-%m-%d")(d3.timeDay.offset(start, -2))) // 日期偏移
console.log(d3.timeDay.filter(d => d.getDay() === 0).range(start, end).map(d3.timeFormat("%Y-%m-%d"))) // 过滤出周日的日期数组
console.log(d3.timeDays(start, end).map(d3.timeFormat("%Y-%m-%d"))) // 生成两个日期之间的所有日期

多列柱状图

堆叠柱状图也可以很方便的转换成多列柱状图,只需要下列两步:

  1. 柱状图的宽度 = xScale.bandwidth / 4
  2. x = xScale() + 柱状图的宽度 * index

image.png

bounds.selectAll("g")
.data(dataset)
.join("g")
.selectAll("rect")
.data((d) => d)
.join("rect")
.attr("x", (d, i, nodes) => {
    return (xScale(data[i].month) +(xScale.bandwidth() / 4) * nodes[i].parentNode.__data__.index);
}) // 改x位置
.attr("y", (d) => yScale(d[1] - d[0]))
.attr("width", xScale.bandwidth() / 4) // 改宽度
.attr("height", (d) => yScale(d[0]) - yScale(d[1]))
.attr("fill", (d, i, nodes) => {
    return color(nodes[i].parentNode.__data__.key);
});

总结

最后点一下文章开头提到的「它们旨在比较不同类别的总值」,也就是通过排序实现的,排序的规则是不同类别的总数进行排序,也就是apples, bananas, cherries, dates这四天的总数,对应的结果就是 apples的总数最大,所以apples显示在最下面(y从0开始),当然这是通过order可以配置的。

同一维度,不同类别的数据都可以采用堆叠数据的处理来进行对比。

本文正在参加「金石计划 . 瓜分6万现金大奖」