D3.js 中的堆叠条形图不堆叠

问题描述 投票:0回答:1

我是 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) #HERE IS WHERE THE ERROR OCCURS
        (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);

我尝试不分组,但最终将条形放置在彼此后面而不是顶部。

条形在彼此后面而不是在顶部:

javascript d3.js bar-chart
1个回答
0
投票

准备数据

问题的核心在于你的数据并不完整

Full Join
不是每个人都在做每个项目

代码

const projectToPersonMap = d3.index(transformedData, d => d.project, d => d.person);

将创建一个二维

map
,其中
project
person
作为键。

打电话

projectToPersonMap.get('Project a').get('Frodo Baggins')

将会产出

{"person": "Frodo Baggins","project": "Project a","duration": 21}

但是打电话

projectToPersonMap.get('Project a').get('Samwise Gamgee')

将是

undefined

d3.stack
将在
Full Join
projects
之间运行
persons
。你必须注意这种关系缺失的情况:

const stackedData = d3.stack()
    // use unique people - do not repeat people in the keys array
    .keys(d3.union(transformedData.map(d => d.person)))
    .value(([project, group], key) => {
    // in this case the project map might not have an entry for the given person
    if (!group.has(key)) {
        return 0; // i.e. a duration of 0
    } else {
        return group.get(key).duration;
    }
})(projectToPersonMap);

生成的

stackedData
是一个如下所示的数组:

// a         f       g       d       h 
[ 0,21], [ 0, 0], [0, 0], [0,  0], [0, 0] // Frodo Baggins
[21,53], [ 0, 0], [0, 0], [0,  0], [0, 0] // Gandalf
[53,53], [ 0,88], [0,27], [0,148], [0,34] // Samwise Gamgee

其中每个 2 元素数组表示

person
project
的堆叠持续时间。任何开始和结束持续时间相同的 2 元素数组(例如
[53,53]
代表 Samwise Gamgee ~ Project a)意味着此人没有在给定项目上工作(持续时间为 0)。

绘图

您可以使用

join()
创建您的
rect
元素。 但是,这将创建
rect
高度为 0 的元素。

另一种选择是将

filter()
enter()
append()
一起使用:

chartGroup
  .selectAll('g.person')
  .data(stackedData)
  // this group contains all rects for a person
  .join('g')
  .attr('class', d => {
    return `person ${encodeURI(d.key.replace(' ', '-').toLowerCase())}`;
  })
  // the color for that person
  .attr('fill', d => color(d.key))
  .selectAll('rect')
  .data(d => d)
  .enter()
  //filter those rect with 0 height (missing person to project combination)
  .filter(d => {
    // check if the duration is > 0
    return (d[1] - d[0]) > 0;
  })
  .append('rect')
  .attr('class', d => {
    return `project ${encodeURI(d.data[0].replace(' ', '-').toLowerCase())}`;
  })
  // the x position and width comes from the band scale
  .attr('x', d => {
    return x(d.data[0]);
  })
  .attr('width', x.bandwidth())
  // add y position and height
  .attr('y', d => y(d[1]))
  .attr('height', d => (y(d[0]) - y(d[1])));

为了使用

d3.scaleLinear
创建 y 尺度,您还必须确定 最长项目的持续时间

// use rollup to get the time spend on each project
const totalDurationPerProject = d3.rollup(
  transformedData,
  // sum up the duration for each project
  arrayOfAllEntriesForProject => d3.sum(arrayOfAllEntriesForProject, d => d.duration),
  // the projects are the keys to grouping the data
  entry => entry.project
);
// get the max duration across all projects
const maxProjectDuration = d3.max([...projects].map(project => totalDurationPerProject.get(project)));

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},
  ]


  // individual people and projects
  const people = d3.union(transformedData.map(d => d.person));
  const projects = d3.union(transformedData.map(d => d.project));

  // use rollup to get the time spend on each project
  const totalDurationPerProject = d3.rollup(
    transformedData,
    // sum up the duration for each project
    arrayOfAllEntriesForProject => d3.sum(arrayOfAllEntriesForProject, d => d.duration),
    // the projects are the keys to grouping the data
    entry => entry.project
  );
  // get the max duration across all projects
  const maxProjectDuration = d3.max([...projects].map(project => totalDurationPerProject.get(project)));

  // create the map project -> person
  const projectToPersonMap = d3.index(transformedData, d => d.project, d => d.person);

  const stackedData = d3.stack()
    .keys(people)
    .value(([project, group], key) => {
      if (!group.has(key)) {
        return 0;
      } else {
        return group.get(key).duration;
      }
    })(projectToPersonMap);

  // we need to add some margin to see the full axis
  const margin = {top: 10, bottom: 150, left: 30, right: 10};
  // static width and height - change this to whatever suits you
  const width = 500;
  const height = 300;

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

  const x = d3.scaleBand()
    .domain(projects)
    .range([0, width])
    .padding([0.2])

  const y = d3.scaleLinear(
    // the domain from max project duration
    [0, maxProjectDuration],
    // over the entire height
    [height, 0]
  );

  const svg = d3.select('#stacked-chart')
    .append('svg')
    // set the size of the chart
    .attr('width', width + margin.left + margin.right)
    .attr('height', height + margin.top + margin.bottom)

  const chartGroup = svg.append('g')
    .attr('class', 'bars')
    .attr('transform', `translate(${margin.left} ${margin.top})`);


  chartGroup
    .selectAll('g.person')
    .data(stackedData)
    .join('g')
    .attr('class', d => {
      return `person ${encodeURI(d.key.replace(' ', '-').toLowerCase())}`;
    })
    .attr('fill', d => color(d.key))
    .selectAll('rect')
    .data(d => d)
    .enter()
    // optionally filter those bars which are of zero height (missing person to project combination)
    .filter(d => {
      return (d[1] - d[0]) > 0;
    })
    .append('rect')
    .attr('class', d => {
      return `project ${encodeURI(d.data[0].replace(' ', '-').toLowerCase())}`;
    })
    // the x position and width comes from the band scale
    .attr('x', d => {
      return x(d.data[0]);
    })
    .attr('width', x.bandwidth())
    // add y position and height
    .attr('y', d => y(d[1]))
    .attr('height', d => (y(d[0]) - y(d[1])));

  // add x and y axis
  svg.append('g')
    .attr('class', 'x-axis')
    .attr('transform', `translate(${margin.left} ${height + margin.top})`)
    .call(d3.axisBottom(x));

  svg.append('g')
    .attr('class', 'y-axis')
    .attr('transform', `translate(${margin.left} ${margin.top})`)
    .call(d3.axisLeft(y));

  // legend at the bottom
  const legendRowHeight = 15;
  const legendRowMargin = 3;

  const legend = svg.append('g')
    .attr('class', 'legend')
    .attr('transform', `translate(${margin.left} ${margin.top + height + 20})`)
  const legendEntryPerPerson = legend.selectAll('g.person')
    .data(people)
    .join('g')
    .attr('class', d => `person ${encodeURI(d.replace(' ', '-').toLowerCase())}`)
    .attr('transform', (d, i) => `translate(20 ${i * (legendRowHeight + legendRowMargin)})`)

  legendEntryPerPerson.append('rect')
    .attr('x', 0).attr('y', 0)
    .attr('width', legendRowHeight).attr('height', legendRowHeight)
    .attr('fill', d => color(d));

  legendEntryPerPerson.append('text')
    .attr('x', (legendRowHeight + legendRowMargin)).attr('y', 0)
    .attr('dy', legendRowHeight)
    .text(d => d);
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Stacked Bar Charts</title>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
<div style="text-align: center;">
  <h1>Stacked Bar Charts</h1>
  <div id="stacked-chart"></div>
</div>
</body>
</html>

© www.soinside.com 2019 - 2024. All rights reserved.