在圆角环形图中避免重叠

huangapple go评论68阅读模式
英文:

Escape overlap in doughnut chart with rounded corners

问题

I've created a doughnut chart.

I've tried to make an arc with rounded corners, but couldn't succeed due to my poor maths. So I added circles on startAngle and endAngle of each arc. Added lineWidth to imitate spacing.

The problem is overlap - circles on the end of each angle overlap the next arc.
How to escape overlap with this design remaining?

Here's the code:

<canvas id="myCanvas"></canvas>

<!-- begin snippet: js hide: false console: true babel: false -->

<!-- language: lang-js -->

"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();

<!-- language: lang-html -->

<canvas id="myCanvas"></canvas>

<!-- end snippet -->
英文:

I've created a doughnut chart.

I've tried to make an arc with rounded corners, but couldn't succeed due to my poor maths. So I added circles on startAngle and endAngle of each arc. Added lineWidth to imitate spacing.

The problem is overlap - circles on the end of each angle overlap the next arc.
How to escape overlap with this design remaining?

Here's the code:

<canvas id="myCanvas"></canvas>

<!-- begin snippet: js hide: false console: true babel: false -->

<!-- language: lang-js -->

&quot;use strict&quot;;
const canvas = document.getElementById(&quot;myCanvas&quot;);
canvas.width = 400;
canvas.height = 400;
const ctx = canvas.getContext(&quot;2d&quot;);
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) =&gt; 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],
&quot;#FFF&quot;,
70,
10
);
startAngle += sliceAngle;
colorIndex++;
}
}
}
let CustomDoughnut = new Doughnut({
canvas,
padding: 20,
spacing: 0,
data: {
TV: 5,
Desktop: 5,
Phone: 5,
Laptop: 5,
},
colors: [&quot;#80DEEE&quot;, &quot;#FFE0E2&quot;, &quot;#FFABE1&quot;, &quot;#CE93DE&quot;],
});
CustomDoughnut.draw();

<!-- language: lang-html -->

&lt;canvas id=&quot;myCanvas&quot;&gt;&lt;/canvas&gt;

<!-- end snippet -->

答案1

得分: 1

不要回答我要翻译的问题。以下是要翻译的代码部分:

How about something simpler like just drawing with the stroke...  
See sample code below: 

<!-- begin snippet: js hide: false console: true babel: false -->

<!-- language: lang-js -->
const canvas = document.getElementById("myCanvas");
canvas.width = 200;
canvas.height = 200;

const ctx = canvas.getContext("2d");
ctx.lineCap = "round"
const PI = Math.PI;
const TAU = PI * 2;

function drawSlice(x, y, radius, startAngle, endAngle, color, lineWidth) {
  ctx.beginPath();
  ctx.strokeStyle = color;
  ctx.lineWidth = lineWidth
  ctx.arc(x, y, 80, startAngle, endAngle);
  ctx.stroke();
}

class Doughnut {
  constructor(options) {
    this.options = options;
    this.x = canvas.width / 2;
    this.y = 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() {
    const DELTA = TAU - this.options.padding * Object.keys(this.options.data).length
    let colorIndex = 0;
    let startAngle = -PI / 2;

    for (const i in this.options.data) {
      const value = this.options.data[i];
      const sliceAngle = (DELTA * value) / this.totalValue;
      
      // draw the white padding
      drawSlice(this.x,this.y, this.radius, startAngle, startAngle+this.options.padding, "white",40);
      startAngle += this.options.padding;
      const endAngle = startAngle + sliceAngle - this.options.spacing / 100;
      // draw the actual color
      drawSlice(this.x,this.y, this.radius, startAngle, endAngle, this.colors[colorIndex % this.colors.length],40);

      startAngle += sliceAngle;
      colorIndex++;
    }
    // redraw the initial color to complete overlap
    drawSlice(this.x,this.y, this.radius, -PI / 2, -PI / 2+this.options.padding, "white",40);
    drawSlice(this.x, this y, this.radius, -PI / 2+this.options.padding, -PI / 2+2*this.options.padding, this.colors[0], 40);
  }
}

let CustomDoughnut = new Doughnut({
  padding: 0.03,
  spacing: 0,
  data: {
    TV: 4000, Desktop: 1200, Phone: 212, Laptop: 40
  },
  colors: ["red", "blue", "cyan", "pink"],
});

CustomDoughnut.draw();

<!-- language: lang-html -->

<canvas id="myCanvas"></canvas>

<!-- end snippet -->
You can see that I've refactored your `function drawSlice` no more save, restore or translate, and we call the arc just once, I did leave the radius hardcoded to 80 you can change that later.
英文:

How about something simpler like just drawing with the stroke...
See sample code below:

<!-- begin snippet: js hide: false console: true babel: false -->

<!-- language: lang-js -->

const canvas = document.getElementById(&quot;myCanvas&quot;);
canvas.width = 200;
canvas.height = 200;
const ctx = canvas.getContext(&quot;2d&quot;);
ctx.lineCap = &quot;round&quot;
const PI = Math.PI;
const TAU = PI * 2;
function drawSlice(x, y, radius, startAngle, endAngle, color, lineWidth) {
ctx.beginPath();
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth
ctx.arc(x, y, 80, startAngle, endAngle);
ctx.stroke();
}
class Doughnut {
constructor(options) {
this.options = options;
this.x = canvas.width / 2;
this.y = canvas.height / 2;
this.colors = options.colors;
this.totalValue = [...Object.values(this.options.data)].reduce( (a, b) =&gt; a + b, 0);
this.radius = Math.min(this.x, this.y) - options.padding;
}
draw() {
const DELTA = TAU - this.options.padding * Object.keys(this.options.data).length
let colorIndex = 0;
let startAngle = -PI / 2;
for (const i in this.options.data) {
const value = this.options.data[i];
const sliceAngle = (DELTA * value) / this.totalValue;
// draw the white padding
drawSlice(this.x,this.y, this.radius, startAngle, startAngle+this.options.padding, &quot;white&quot;,40);
startAngle += this.options.padding;
const endAngle = startAngle + sliceAngle - this.options.spacing / 100;
// draw the actual color
drawSlice(this.x,this.y, this.radius, startAngle, endAngle, this.colors[colorIndex % this.colors.length],40);
startAngle += sliceAngle;
colorIndex++;
}
// redraw the initial color to complete overlap
drawSlice(this.x,this.y, this.radius, -PI / 2, -PI / 2+this.options.padding, &quot;white&quot;,40);
drawSlice(this.x, this.y, this.radius, -PI / 2+this.options.padding, -PI / 2+2*this.options.padding, this.colors[0], 40);
}
}
let CustomDoughnut = new Doughnut({
padding: 0.03,
spacing: 0,
data: {
TV: 4000, Desktop: 1200, Phone: 212, Laptop: 40
},
colors: [&quot;red&quot;, &quot;blue&quot;, &quot;cyan&quot;, &quot;pink&quot;],
});
CustomDoughnut.draw();

<!-- language: lang-html -->

&lt;canvas id=&quot;myCanvas&quot;&gt;&lt;/canvas&gt;

<!-- end snippet -->

You can see that I've refactored your function drawSlice no more save, restore or translate, and we call the arc just once, I did left the radius hardcoded to 80 you can change that later.

huangapple
  • 本文由 发表于 2023年3月9日 23:10:41
  • 转载请务必保留本文链接:https://go.coder-hub.com/75686487.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定