问题描述
我有一个项目需要创建堆积条形图。此条形图需要在顶部有一个圆形边框。我找到了一些关于如何将条形图的顶部边框变圆的文档。我遵循的示例可以在这里找到:Rounded top corners for bar chart.
`
M${x(item.name)},${y(item.value) + ry}
a${rx},${ry} 0 0 1 ${rx},${-ry}
h${x.bandwidth() - 2 * rx}
a${rx},${ry}
v${height - y(item.value) - ry}
h${-x.bandwidth()}Z
`
您还需要声明两个变量 rx
和 ry
,用于定义角的锐度。
我面临的问题是我无法让它与堆积条形图一起工作。顶部堆栈需要在顶部四舍五入。此外,当顶部堆栈为 0 零时,下一个堆栈需要四舍五入。这样无论包含什么数据,条形图的顶部总是四舍五入。
我添加了一个精简的片段。它包括一个按钮来转换我删除(设置为零)顶部堆栈的位置。这个片段当然还包括转角所需的属性。但它不会四舍五入。
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}
h${this.xScale.bandwidth() - 2 * rx}
a${rx},${ry}
v${this.height - this.yScale(item.value) - ry}
h${-this.xScale.bandwidth()}Z
`
),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",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>
解决方法
将 rect
的绘图更改为 path
。因此,.append("rect")
变为 .append("path")
,您不再需要属性 x
、y
、height
和 width
。
要仅舍入顶部堆栈,您可以在 d3 绘制最后一个子组 (rx
) 之前将 ry
和 d[1] - d[0] == d.data[subgroups[subgroups.length-1]]
设置为 12,在本例中为“公共”,否则将它们设置为 0。
最后,对于您的最后一个问题:
当顶部堆栈为 0 零时,下一个堆栈需要四舍五入
堆栈按顺序绘制。在每次绘制之前,先找出下一个堆栈/子组是否为零,如果是,则设置rx
和ry
= 12。要找出这一点,您需要获取当前正在绘制的子组,因此您可以确定下一个子组是什么,并获得该子组的值。
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
v${this.height - this.yScale(0)}
h${-this.xScale.bandwidth()}Z
`
),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}
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
h${this.xScale.bandwidth()}
a0,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",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>