You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

D3.js堆叠柱状图无法堆叠,数据分组报错求助

D3.js堆叠柱状图数据分组与堆叠问题解决

问题描述

我是D3.js新手,尝试创建堆叠柱状图但无法正确堆叠数值。调用stack()的.value(([, group], key) => group.get(key).duration)时出现TypeError: Cannot read properties of undefined (reading 'duration')错误,直接绘制时柱子还会重叠而非堆叠。

给定数据

const transformedData = [
  { person: 'Frodo Baggins', project: 'Project a', duration: 21 },
  { person: 'Gandalf', project: 'Project a', duration: 32 },
  { person: 'Samwise Gamgee', project: 'Project f', duration: 88 },
  { person: 'Samwise Gamgee', project: 'Project g', duration: 27 },
  { person: 'Samwise Gamgee', project: 'Project d', duration: 148 },
  { person: 'Samwise Gamgee', project: 'Project h', duration: 34 },
]

原错误代码

const margin = {top: 10, right: 30, bottom: 20, left: 50},
width = 460 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
const svg = d3.select(document.body)
  .append("svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom)
  .append("g")
  .attr("transform",
        "translate(" + margin.left + "," + margin.top + ")");

const stackedData = d3.stack()
  .keys(transformedData.map(d => d.person))
  .value(([, group], key) => group.get(key).duration) // 此处报错
  (d3.index(transformedData, d => d.project, d => d.person));

const x = d3.scaleBand()
  .domain(projects)
  .range([0, width])
  .padding([0.2])
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).tickSizeOuter(0));

const y = d3.scaleLinear()
  .domain([0, totalDuration])
  .range([ height, 0 ]);
svg.append("g")
.call(d3.axisLeft(y));

const color = d3.scaleOrdinal()
  .domain(people)
  .range(['#e41a1c','#377eb8','#4daf4a'])

svg.append("g")
  .selectAll("g")
  .data(stackedData)
  .enter()
  .append("g")
  .attr("fill", d => color(d.key))
  .selectAll("rect")
  .data((d) => projects.map((project) => ({
    project,
    duration: transformedData.reduce((acc, i) => i.project === project && i.person === d.key ? acc + i.duration : acc, 0)
  })))
  .enter()
  .append("rect")
  .attr('x', (d, i) => (i * width) / projects.length)
  .attr('y', (d) => height - (d.duration / totalDuration) * height)
  .attr('height', (d) => (d.duration / totalDuration) * height)
  .attr('width', width / projects.length - 2);

错误表现

直接绘制时柱子重叠,效果如下:
柱子重叠而非堆叠


解决方案

1. 修正数据分组与stack配置

报错原因是d3.index返回的结构和.value中预期的不匹配,且.keys直接用transformedData.map(d => d.person)会产生重复的人员值。先预处理数据,得到适配stack的结构化格式:

// 获取唯一人员、项目列表
const people = [...new Set(transformedData.map(d => d.person))];
const projects = [...new Set(transformedData.map(d => d.project))];

// 转换为「项目-各人员时长」的结构化数据,无数据时补0
const preparedData = projects.map(project => {
  const obj = { project };
  people.forEach(person => {
    const matchItem = transformedData.find(d => d.project === project && d.person === person);
    obj[person] = matchItem ? matchItem.duration : 0;
  });
  return obj;
});

// 生成堆叠数据
const stackedData = d3.stack()
  .keys(people) // 使用唯一人员列表作为堆叠键
  (preparedData);

2. 补全比例尺与坐标轴缺失逻辑

原代码中projects和totalDuration未定义,补充计算逻辑:

// 计算所有项目的最大总时长
const totalDuration = d3.max(preparedData, d => {
  return people.reduce((sum, person) => sum + d[person], 0);
});

// X轴比例尺与坐标轴
const x = d3.scaleBand()
  .domain(projects)
  .range([0, width])
  .padding(0.2);

svg.append("g")
  .attr("transform", `translate(0, ${height})`)
  .call(d3.axisBottom(x).tickSizeOuter(0));

// Y轴比例尺与坐标轴
const y = d3.scaleLinear()
  .domain([0, totalDuration])
  .range([height, 0]);

svg.append("g")
  .call(d3.axisLeft(y));

3. 修正柱状图绘制逻辑

利用stack返回的上下边界数据直接绘制,无需手动计算:

svg.append("g")
  .selectAll("g")
  .data(stackedData)
  .join("g")
  .attr("fill", d => color(d.key))
  .selectAll("rect")
  .data(d => d)
  .join("rect")
  .attr("x", d => x(d.data.project))
  .attr("y", d => y(d[1])) // d[1]是堆叠段的上边界
  .attr("height", d => y(d[0]) - y(d[1])) // d[0]是堆叠段的下边界
  .attr("width", x.bandwidth());

完整修正代码

const transformedData = [
  { person: 'Frodo Baggins', project: 'Project a', duration: 21 },
  { person: 'Gandalf', project: 'Project a', duration: 32 },
  { person: 'Samwise Gamgee', project: 'Project f', duration: 88 },
  { person: 'Samwise Gamgee', project: 'Project g', duration: 27 },
  { person: 'Samwise Gamgee', project: 'Project d', duration: 148 },
  { person: 'Samwise Gamgee', project: 'Project h', duration: 34 },
];

const margin = {top: 10, right: 30, bottom: 20, left: 50},
width = 460 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;

const svg = d3.select(document.body)
  .append("svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom)
  .append("g")
  .attr("transform", `translate(${margin.left}, ${margin.top})`);

// 预处理数据
const people = [...new Set(transformedData.map(d => d.person))];
const projects = [...new Set(transformedData.map(d => d.project))];

const preparedData = projects.map(project => {
  const obj = { project };
  people.forEach(person => {
    const matchItem = transformedData.find(d => d.project === project && d.person === person);
    obj[person] = matchItem ? matchItem.duration : 0;
  });
  return obj;
});

// 生成堆叠数据
const stackedData = d3.stack()
  .keys(people)
  (preparedData);

// 计算总时长最大值
const totalDuration = d3.max(preparedData, d => {
  return people.reduce((sum, person) => sum + d[person], 0);
});

// X轴配置
const x = d3.scaleBand()
  .domain(projects)
  .range([0, width])
  .padding(0.2);

svg.append("g")
  .attr("transform", `translate(0, ${height})`)
  .call(d3.axisBottom(x).tickSizeOuter(0));

// Y轴配置
const y = d3.scaleLinear()
  .domain([0, totalDuration])
  .range([height, 0]);

svg.append("g")
  .call(d3.axisLeft(y));

// 颜色比例尺
const color = d3.scaleOrdinal()
  .domain(people)
  .range(['#e41a1c','#377eb8','#4daf4a']);

// 绘制堆叠柱状图
svg.append("g")
  .selectAll("g")
  .data(stackedData)
  .join("g")
  .attr("fill", d => color(d.key))
  .selectAll("rect")
  .data(d => d)
  .join("rect")
  .attr("x", d => x(d.data.project))
  .attr("y", d => y(d[1]))
  .attr("height", d => y(d[0]) - y(d[1]))
  .attr("width", x.bandwidth());

内容的提问来源于stack exchange,提问作者Way2Pin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 16:47:04