You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何避免带圆角的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>

解决方案

核心思路是缩短每个切片的弧段长度,让两端的圆形刚好覆盖弧的端点,避免圆形超出到下一个切片的范围。具体修改如下:

  1. 在drawSlice函数中,计算圆形半径对应的角度偏移量,用这个偏移量调整弧的起始和结束角度,让弧的两端缩进,刚好被圆形覆盖。
  2. 保持两端圆形的绘制逻辑不变,确保圆角效果完整。

修改后的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 17:08:09