Reputation: 1183
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
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