如何避免带圆角的Doughnut Chart(环形图)出现重叠问题
解决环形图圆角圆形与下一段弧重叠的问题
我制作了一个环形图,因数学基础薄弱,无法直接绘制带圆角的弧,于是在每个弧的startAngle和endAngle位置添加圆形,并用lineWidth模拟间距。目前存在的问题是:每个角度末端的圆形会与下一段弧重叠。请问如何在保留当前设计的前提下规避该重叠问题?
原代码
JavaScript
"use strict"; const canvas = document.getElementById("myCanvas"); canvas.width = 400; canvas.height = 400; const ctx = canvas.getContext("2d"); const PI = Math.PI; const TAU = PI * 2; function drawSlice( ctx, x, y, radius, startAngle, endAngle, fillColor, strokeColor, cutout, lineWidth ) { ctx.fillStyle = fillColor; ctx.strokeStyle = strokeColor; ctx.save(); ctx.translate(x, y); ctx.beginPath(); ctx.arc( ((radius + (radius * cutout) / 100) / 2) * Math.cos(endAngle), ((radius + (radius * cutout) / 100) / 2) * Math.sin(endAngle), (radius - (radius * cutout) / 100) / 2 + lineWidth / 2, 0, TAU ); ctx.closePath(); ctx.fill(); ctx.lineWidth = lineWidth; ctx.stroke(); ctx.beginPath(); ctx.arc( ((radius + (radius * cutout) / 100) / 2) * Math.cos(startAngle), ((radius + (radius * cutout) / 100) / 2) * Math.sin(startAngle), (radius - (radius * cutout) / 100) / 2 + lineWidth / 2, TAU, 0 ); ctx.fill(); ctx.stroke(); ctx.restore(); ctx.save(); ctx.beginPath(); ctx.arc(x, y, radius, startAngle, endAngle); ctx.arc(x, y, (radius * cutout) / 100, endAngle, startAngle, true); ctx.closePath(); ctx.fill(); ctx.stroke(); // enables to see overlap ctx.restore(); } class Doughnut { constructor(options) { this.options = options; this.canvas = options.canvas; this.ctx = ctx; this.x = this.canvas.width / 2; this.y = this.canvas.height / 2; this.colors = options.colors; this.totalValue = [...Object.values(this.options.data)].reduce( (a, b) => a + b, 0 ); this.radius = Math.min(this.x, this.y) - options.padding; } draw() { let colorIndex = 0; let startAngle = -PI / 2; for (const i in this.options.data) { const value = this.options.data[i]; const sliceAngle = (TAU * value) / this.totalValue; const endAngle = startAngle + sliceAngle - this.options.spacing / 100; drawSlice( this.ctx, this.x, this.y, this.radius, startAngle, endAngle, this.colors[colorIndex % this.colors.length], "#FFF", 70, 10 ); startAngle += sliceAngle; colorIndex++; } } } let CustomDoughnut = new Doughnut({ canvas, padding: 20, spacing: 0, data: { TV: 5, Desktop: 5, Phone: 5, Laptop: 5, }, colors: ["#80DEEE", "#FFE0E2", "#FFABE1", "#CE93DE"], }); CustomDoughnut.draw();
HTML
<canvas id="myCanvas"></canvas>
解决方案
核心思路是缩短每个切片的弧段长度,让两端的圆形刚好覆盖弧的端点,避免圆形超出到下一个切片的范围。具体修改如下:
- 在
drawSlice函数中,计算圆形半径对应的角度偏移量,用这个偏移量调整弧的起始和结束角度,让弧的两端缩进,刚好被圆形覆盖。 - 保持两端圆形的绘制逻辑不变,确保圆角效果完整。
修改后的drawSlice函数代码:
function drawSlice( ctx, x, y, radius, startAngle, endAngle, fillColor, strokeColor, cutout, lineWidth ) { ctx.fillStyle = fillColor; ctx.strokeStyle = strokeColor; // 计算关键参数 const innerRadius = (radius * cutout) / 100; const middleRadius = (radius + innerRadius) / 2; const circleRadius = (radius - innerRadius) / 2 + lineWidth / 2; // 计算圆形对应的角度偏移:用反正弦函数求圆形半径在中间半径上的角度占比 const angleOffset = Math.asin(circleRadius / middleRadius); // 绘制两端的圆形 ctx.save(); ctx.translate(x, y); // 末端圆形 ctx.beginPath(); ctx.arc( middleRadius * Math.cos(endAngle), middleRadius * Math.sin(endAngle), circleRadius, 0, TAU ); ctx.closePath(); ctx.fill(); ctx.lineWidth = lineWidth; ctx.stroke(); // 起始端圆形 ctx.beginPath(); ctx.arc( middleRadius * Math.cos(startAngle), middleRadius * Math.sin(startAngle), circleRadius, 0, TAU ); ctx.fill(); ctx.stroke(); ctx.restore(); // 绘制缩进后的弧段,避免和下一个切片重叠 ctx.save(); ctx.beginPath(); // 弧的起始角度增加偏移量,结束角度减少偏移量 ctx.arc(x, y, radius, startAngle + angleOffset, endAngle - angleOffset); ctx.arc(x, y, innerRadius, endAngle - angleOffset, startAngle + angleOffset, true); ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.restore(); }
原理说明
angleOffset:计算圆形在环形中间半径上对应的角度,这个角度就是需要把弧两端缩进的距离,确保弧的端点刚好被圆形覆盖,不会延伸到下一个切片的区域。- 修改后的弧段起始和结束角度都做了偏移,这样弧的两端就不会和下一个切片的圆形重叠,同时圆形依然能完整呈现圆角效果。
内容的提问来源于stack exchange,提问作者Nikita Kopot
相关产品推荐
相关产品推荐

