Stephen
Stephen

Reputation: 1183

D3 How to add rounded top border to a stacked bar chart

I have a project where I need to create a stacked bar chart. This bar chart needs to have a rounded border at the top. I have found some documentation on how to round the top border of bar chart. The example I followed can be found here: Rounded top corners for bar chart.

It says to add a attribute with the following:

`
 M${x(item.name)},${y(item.value) + ry}
 a${rx},${ry} 0 0 1 ${rx},${-ry}
 h${x.bandwidth() - 2 * rx}
 a${rx},${ry} 0 0 1 ${rx},${ry}
 v${height - y(item.value) - ry}
 h${-x.bandwidth()}Z
`

You also need to declare two variables rx and ry that define the sharpness of the corner.

The problem I am facing is that I cannot get it to work with my stacked bar chart. The top stack needs to be rounded at the top. Also when the top stack is 0 zero the next stack needs to be rounded. So that the top of the bar is always rounded no matter what data is included.

I have added a trimmed down snippet. It includes a button to transition where I remove(set to zero) the top stacks. This snippet of course also includes the attribute needed to round the corner. But it doesn't get rounded.

this.width = 400;
this.height = 200;
var margin = {
  top: 20,
  right: 20,
  bottom: 30,
  left: 40
}

this.index = 0;

this.svg = d3
  .select(".canvas")
  .classed("svg-container", true)
  .append("svg")
  .attr("class", "chart")
  .attr(
    "viewBox",
    `0 0 ${this.width} ${this.height}`
  )
  .attr("preserveAspectRatio", "xMinYMin meet")
  .classed("svg-content-responsive", true)
  .append("g");

const scale = [0, 1200];

// set the scales
this.xScale = d3
  .scaleBand()
  .range([0, width])
  .padding(0.3);

this.yScale = d3.scaleLinear().range([this.height, 0]);

var bars = this.svg.append("g").attr("class", "bars");

const update = data => {
  const scale = [0, 1200];

  // Update scales.
  this.xScale.domain(data.map(d => d.key));
  this.yScale.domain([scale[0], scale[1]]);

  const subgroups = ["home", "work", "public"];

  var color = d3
    .scaleOrdinal()
    .domain(subgroups)
    .range(["#206BF3", "#171D2C", "#8B0000"]);

  var stackData = d3.stack().keys(subgroups)(data);

  const rx = 12;
  const ry = 12;

  // Set up transition.
  const dur = 1000;
  const t = d3.transition().duration(dur);

  bars
    .selectAll("g")
    .data(stackData)
    .join(
      enter => enter
      .append("g")
      .attr("fill", d => color(d.key)),

      null, // no update function

      exit => {
        exit
          .transition()
          .duration(dur / 2)
          .style("fill-opacity", 0)
          .remove();
      }
    ).selectAll("rect")
    .data(d => d, d => d.data.key)
    .join(
      enter => enter
      .append("rect")
      .attr("class", "bar")
      .attr("x", d => {
        return this.xScale(d.data.key);
      })
      .attr("y", () => {
        return this.yScale(0);
      })
      .attr("height", () => {
        return this.height - this.yScale(0);
      })
      .attr("width", this.xScale.bandwidth())

      .attr(
        'd',
        item =>
        `M${this.xScale(item.name)},${this.yScale(item.value) + ry}
        a${rx},${ry} 0 0 1 ${rx},${-ry}
        h${this.xScale.bandwidth() - 2 * rx}
        a${rx},${ry} 0 0 1 ${rx},${ry}
        v${this.height - this.yScale(item.value) - ry}
        h${-this.xScale.bandwidth()}Z
        `
      ),
      null,
      exit => {
        exit
          .transition()
          .duration(dur / 2)
          .style("fill-opacity", 0)
          .remove();
      }
    )
    .transition(t)
    .delay((d, i) => i * 20)
    .attr("x", d => this.xScale(d.data.key))
    .attr("y", d => {
      return this.yScale(d[1]);
    })
    .attr("width", this.xScale.bandwidth())
    .attr("height", d => {
      return this.yScale(d[0]) - this.yScale(d[1]);
    });
};

const data = [
  [{
      key: "1",
      home: 282,
      work: 363,
      public: 379
    },
    {
      key: "2",
      home: 232,
      work: 432,
      public: 0
    }
  ],
  [{
      key: "1",
      home: 282,
      work: 363,
      public: 379
    },
    {
      key: "2",
      home: 232,
      work: 0,
      public: 0
    }
  ]
];

update(data[this.index]);

const swap = document.querySelector(".swap");
swap.addEventListener("click", () => {
  if (this.index < 1) this.index += 1;
  else this.index = 0;
  update(data[this.index]);
});
<button class="swap">swap</button>
<div class="canvas"></div>
<script src="https://d3js.org/d3.v6.js"></script>

Upvotes: 1

Views: 1924

Answers (1)

Sharon Choong
Sharon Choong

Reputation: 596

Change the drawing of rect to path. So .append("rect") becomes .append("path"), and you don't need the attributes x, y, height and width anymore.

To round the top stack only, you can set rx and ry to 12 just before d3 draws the last subgroup (d[1] - d[0] == d.data[subgroups[subgroups.length-1]]), which is "public" in this case, and otherwise set them to 0.

Finally, for your last problem:

when the top stack is 0 zero the next stack needs to be rounded

Stacks are drawn in order. Before each stack is drawn, find out whether the next stack/subgroup is zero, and if so, set rx and ry = 12. To find this out, you need to get the current subgroup being drawn, so you can identify what the next subgroup will be, and get the value of that subgroup.

const current_subgroup = Object.keys(d.data).find(key => d.data[key] === d[1] - d[0]);
const next_subgroup_index = Math.min(subgroups.length - 1, subgroups.indexOf(current_subgroup)+1);
const next_subgroup_data = stackData[next_subgroup_index][i];
            if (next_subgroup_data[1] - next_subgroup_data[0] == 0) { rx = 12; ry = 12; }

this.width = 400;
this.height = 200;
var margin = {
  top: 20,
  right: 20,
  bottom: 30,
  left: 40
}

this.index = 0;

this.svg = d3
  .select(".canvas")
  .classed("svg-container", true)
  .append("svg")
  .attr("class", "chart")
  .attr(
    "viewBox",
    `0 0 ${this.width} ${this.height}`
  )
  .attr("preserveAspectRatio", "xMinYMin meet")
  .classed("svg-content-responsive", true)
  .append("g");

const scale = [0, 1200];

// set the scales
this.xScale = d3
  .scaleBand()
  .range([0, width])
  .padding(0.3);

this.yScale = d3.scaleLinear().range([this.height, 0]);

var bars = this.svg.append("g").attr("class", "bars");

const update = data => {
  const scale = [0, 1200];

  // Update scales.
  this.xScale.domain(data.map(d => d.key));
  this.yScale.domain([scale[0], scale[1]]);

  const subgroups = ["home", "work", "public"];

  var color = d3
    .scaleOrdinal()
    .domain(subgroups)
    .range(["#206BF3", "#171D2C", "#8B0000"]);

  var stackData = d3.stack().keys(subgroups)(data);

  let rx = 12;
  let ry = 12;

  // Set up transition.
  const dur = 1000;
  const t = d3.transition().duration(dur);

  bars
    .selectAll("g")
    .data(stackData)
    .join(
      enter => enter
      .append("g")
      .attr("fill", d => color(d.key)),

      null, // no update function

      exit => {
        exit
          .transition()
          .duration(dur / 2)
          .style("fill-opacity", 0)
          .remove();
      }
    ).selectAll("path")
    .data(d => d, d => d.data.key)
    .join(
      enter => enter
      .append("path")
      .attr("class", "bar")
      .attr(
        'd',
        d =>
        `M${this.xScale(d.data.key)},${this.yScale(0)}
        a0,0 0 0 1 0,0
        h${this.xScale.bandwidth()}
        a0,0 0 0 1 0,0
        v${this.height - this.yScale(0)}
        h${-this.xScale.bandwidth()}Z
        `
      ),
      null,
      exit => {
        exit
          .transition()
          .duration(dur / 2)
          .style("fill-opacity", 0)
          .remove();
      }
    )
    .transition(t)
    .delay((d, i) => i * 20)
    .attr(
      'd',
      (d, i) => {
        //if last subgroup, round the corners of the stack
        if (d[1] - d[0] == d.data[subgroups[subgroups.length-1]]) { rx = 12; ry = 12; }
        else { rx = 0; ry = 0; }
        
        //if next subgroup is zero, round the corners of the stack
        const current_subgroup = Object.keys(d.data).find(key => d.data[key] === d[1] - d[0]);
        const next_subgroup_index = Math.min(subgroups.length - 1, subgroups.indexOf(current_subgroup)+1);
        const next_subgroup_data = stackData[next_subgroup_index][i];
        if (next_subgroup_data[1] - next_subgroup_data[0] == 0) { rx = 12; ry = 12; }
        
        //draw the stack
        if (d[1] - d[0] > 0) {
          return `M${this.xScale(d.data.key)},${this.yScale(d[1]) + ry}
          a${rx},${ry} 0 0 1 ${rx},${-ry}
          h${this.xScale.bandwidth() - 2 * rx}
          a${rx},${ry} 0 0 1 ${rx},${ry}
          v${this.yScale(d[0]) - this.yScale(d[1]) - ry}
          h${-this.xScale.bandwidth()}Z
          `
        } else {
          return `M${this.xScale(d.data.key)},${this.yScale(d[1])}
          a0,0 0 0 1 0,0
          h${this.xScale.bandwidth()}
          a0,0 0 0 1 0,0
          v${this.yScale(d[0]) - this.yScale(d[1]) }
          h${-this.xScale.bandwidth()}Z
          `
        }
      }
    );
};
const data = [
  [{
      key: "1",
      home: 282,
      work: 363,
      public: 379
    },
    {
      key: "2",
      home: 232,
      work: 432,
      public: 0
    }
  ],
  [{
      key: "1",
      home: 282,
      work: 363,
      public: 379
    },
    {
      key: "2",
      home: 232,
      work: 0,
      public: 0
    }
  ]
];

update(data[this.index]);

const swap = document.querySelector(".swap");
swap.addEventListener("click", () => {
  if (this.index < 1) this.index += 1;
  else this.index = 0;
  update(data[this.index]);
});
<button class="swap">swap</button>
<div class="canvas"></div>
<script src="https://d3js.org/d3.v6.js"></script>

Upvotes: 4

Related Questions