2

I am using D3 to make a stacked bar chart (for more artistic purposes than scientific). I want to design my stacked bar chart to be centered around one group, with half above and half below an invisible line, and have the other two groups be on either side of the line.

Currently, my graph looks like this

But I want it to look more like this

My code is here:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Stacked Bar</title>
  </head>
  <body>
    <div class="container">
      <div id="chart"></div>
    </div>

    <script src="https://d3js.org/d3.v7.min.js"></script>
    <script>
      const width = 860,
        height = 400,
        margin = { top: 40, right: 30, bottom: 20, left: 20 };

      const svg = d3
        .select("#chart")
        .append("svg")
        .attr("viewBox", [0, 0, width, height]);

      d3.csv("test.csv").then((data) => {
        let x = d3
          .scaleBand(
            data.map((d) => d.Time),
            [margin.left, width - margin.right]
          )
          .padding([0.2]);

        let y = d3.scaleLinear([0, 500], [height - margin.bottom, margin.top]);

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

        svg.append("g").attr("transform", `translate(${margin.left},0)`);
        //   .call(d3.axisLeft(y).tickSize(-width + margin.left + margin.right));

        //protein,carbs,fiber
        const subgroups = data.columns.slice(1);

        const color = d3.scaleOrdinal(subgroups, [
          "#e41a1c",
          "#377eb8",
          "#4daf4a",
        ]);

        const stackedData = d3.stack().keys(subgroups)(data);

        console.log(stackedData);

        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.Time))
          .attr("y", (d) => y(d[1]))
          .attr("height", (d) => y(d[0]) - y(d[1]))
          .attr("width", x.bandwidth());

        let legendGroup = svg
          .selectAll(".legend-group")
          .data(subgroups)
          .join("g")
          .attr("class", "legend-group");

        legendGroup
          .append("circle")
          .attr("cx", (d, i) => 10 + i * 75)
          .attr("cy", 10)
          .attr("r", 3)
          .attr("fill", (d, i) => color(i));

        legendGroup
          .append("text")
          .attr("x", (d, i) => 20 + i * 75)
          .attr("y", 15)
          .text((d, i) => subgroups[i]);
      });
    </script>
  </body>
</html>

and csv:

Time,team1,team2,middle
0,5,2,70
1,10,13,89
2,4,15,110
3,6,16,145
4,12,2,167
5,42,3,111
6,6,4,108
7,7,5,92
8,8,34,140
9,12,89,190
10,22,90,398
11,42,91,459
12,60,23,256
13,69,13,253
14,43,11,188
15,42,7,167
16,21,9,124
17,16,12,156
18,7,14,167
19,12,13,188

Does anyone know how I could vertically center each line around the middle group? Is this something to do in the data pre-processing or in the graph making itself?

Gerardo Furtado
  • 100,839
  • 9
  • 121
  • 171
atowe
  • 41
  • 2

1 Answers1

0

You have to use the correct offset, in this case d3.offsetWiggle:

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

In this solution I'm flattening the stacked data and getting the extent, which I'll pass to the y scale:

const flatData = stackedData.flat(2);
y.domain(d3.extent(flatData));

Finally, I'm just moving the x axis to the middle of the y range. Also, I'm hardcoding the stack keys, but making the sequence programatically is trivial, as well as some other details you'll have to adjust.

Here's the result:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Stacked Bar</title>
</head>

<body>
  <div class="container">
    <div id="chart"></div>
  </div>

  <script src="https://d3js.org/d3.v7.min.js"></script>
  <script>
    const width = 860,
      height = 400,
      margin = {
        top: 40,
        right: 30,
        bottom: 20,
        left: 20
      };

    const svg = d3
      .select("#chart")
      .append("svg")
      .attr("viewBox", [0, 0, width, height]);

    const csv = `Time,team1,team2,middle
0,5,2,70
1,10,13,89
2,4,15,110
3,6,16,145
4,12,2,167
5,42,3,111
6,6,4,108
7,7,5,92
8,8,34,140
9,12,89,190
10,22,90,398
11,42,91,459
12,60,23,256
13,69,13,253
14,43,11,188
15,42,7,167
16,21,9,124
17,16,12,156
18,7,14,167
19,12,13,188`;

    const data = d3.csvParse(csv);

    let x = d3
      .scaleBand(
        data.map((d) => d.Time), [margin.left, width - margin.right]
      )
      .padding([0.2]);

    let y = d3.scaleLinear().range([height - margin.bottom, margin.top]);

    svg.append("g").attr("transform", `translate(${margin.left},0)`);
    //   .call(d3.axisLeft(y).tickSize(-width + margin.left + margin.right));

    //protein,carbs,fiber
    const subgroups = ["team1", "middle", "team2"];

    const color = d3.scaleOrdinal(subgroups, [
      "#377eb8",
      "#4daf4a",
      "#e41a1c"
    ]);

    const stackedData = d3.stack().offset(d3.stackOffsetWiggle).order(d3.stackOrderNone).keys(subgroups)(data);

    const flatData = stackedData.flat(2);

    y.domain(d3.extent(flatData));

    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.Time))
      .attr("y", (d) => y(d[1]))
      .attr("height", (d) => y(d[0]) - y(d[1]))
      .attr("width", x.bandwidth());

    svg
      .append("g")
      .attr("transform", `translate(0,${margin.top + (height - margin.bottom)/2})`)
      .call(d3.axisBottom(x));

    let legendGroup = svg
      .selectAll(".legend-group")
      .data(subgroups)
      .join("g")
      .attr("class", "legend-group");

    legendGroup
      .append("circle")
      .attr("cx", (d, i) => 10 + i * 75)
      .attr("cy", 10)
      .attr("r", 3)
      .attr("fill", (d, i) => color(i));

    legendGroup
      .append("text")
      .attr("x", (d, i) => 20 + i * 75)
      .attr("y", 15)
      .text((d, i) => subgroups[i]);
  </script>
</body>

</html>
Gerardo Furtado
  • 100,839
  • 9
  • 121
  • 171