]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
fix: Always draw full arcs and borders for doughnut slices (#10806)
authorRichard Gibson <richard.gibson@gmail.com>
Sat, 12 Nov 2022 19:47:08 +0000 (14:47 -0500)
committerGitHub <noreply@github.com>
Sat, 12 Nov 2022 19:47:08 +0000 (21:47 +0200)
* test: Add a failing test for single-slice doughnut with offset

* fix: Always draw full arcs and borders for doughnut slices

Fixes #10801

* test: Update existing image

src/elements/element.arc.ts
test/fixtures/controller.doughnut/single-slice-circumference-405.png
test/fixtures/controller.doughnut/single-slice-offset.js [new file with mode: 0644]
test/fixtures/controller.doughnut/single-slice-offset.png [new file with mode: 0644]

index b9b9474e779b5a278a1eaa91788821baab8c0a35..7e19c91158232dfc5d077366556cd06929851dec 100644 (file)
@@ -67,21 +67,18 @@ function rThetaToXY(r: number, theta: number, x: number, y: number) {
 
 
 /**
- * Path the arc, respecting the border radius
- *
- * 8 points of interest exist around the arc segment.
- * These points define the intersection of the arc edges and the corners.
+ * Path the arc, respecting border radius by separating into left and right halves.
  *
  *   Start      End
  *
- *    1---------2    Outer
+ *    1--->a--->2    Outer
  *   /           \
  *   8           3
  *   |           |
  *   |           |
  *   7           4
  *   \           /
- *    6---------5    Inner
+ *    6<---b<---5    Inner
  */
 function pathArc(
   ctx: CanvasRenderingContext2D,
@@ -129,8 +126,10 @@ function pathArc(
   ctx.beginPath();
 
   if (circular) {
-    // The first arc segment from point 1 to point 2
-    ctx.arc(x, y, outerRadius, outerStartAdjustedAngle, outerEndAdjustedAngle);
+    // The first arc segments from point 1 to point a to point 2
+    const outerMidAdjustedAngle = (outerStartAdjustedAngle + outerEndAdjustedAngle) / 2;
+    ctx.arc(x, y, outerRadius, outerStartAdjustedAngle, outerMidAdjustedAngle);
+    ctx.arc(x, y, outerRadius, outerMidAdjustedAngle, outerEndAdjustedAngle);
 
     // The corner segment from point 2 to point 3
     if (outerEnd > 0) {
@@ -148,8 +147,10 @@ function pathArc(
       ctx.arc(pCenter.x, pCenter.y, innerEnd, endAngle + HALF_PI, innerEndAdjustedAngle + Math.PI);
     }
 
-    // The inner arc from point 5 to point 6
-    ctx.arc(x, y, innerRadius, endAngle - (innerEnd / innerRadius), startAngle + (innerStart / innerRadius), true);
+    // The inner arc from point 5 to point b to point 6
+    const innerMidAdjustedAngle = ((endAngle - (innerEnd / innerRadius)) + (startAngle + (innerStart / innerRadius))) / 2;
+    ctx.arc(x, y, innerRadius, endAngle - (innerEnd / innerRadius), innerMidAdjustedAngle, true);
+    ctx.arc(x, y, innerRadius, innerMidAdjustedAngle, startAngle + (innerStart / innerRadius), true);
 
     // The corner segment from point 6 to point 7
     if (innerStart > 0) {
@@ -191,17 +192,12 @@ function drawArc(
   const {fullCircles, startAngle, circumference} = element;
   let endAngle = element.endAngle;
   if (fullCircles) {
-    pathArc(ctx, element, offset, spacing, startAngle + TAU, circular);
-
+    pathArc(ctx, element, offset, spacing, endAngle, circular);
     for (let i = 0; i < fullCircles; ++i) {
       ctx.fill();
     }
-
     if (!isNaN(circumference)) {
-      endAngle = startAngle + circumference % TAU;
-      if (circumference % TAU === 0) {
-        endAngle += TAU;
-      }
+      endAngle = startAngle + (circumference % TAU || TAU);
     }
   }
   pathArc(ctx, element, offset, spacing, endAngle, circular);
@@ -209,39 +205,14 @@ function drawArc(
   return endAngle;
 }
 
-function drawFullCircleBorders(ctx: CanvasRenderingContext2D, element: ArcElement, inner: boolean) {
-  const {x, y, startAngle, pixelMargin, fullCircles} = element;
-  const outerRadius = Math.max(element.outerRadius - pixelMargin, 0);
-  const innerRadius = element.innerRadius + pixelMargin;
-
-  let i;
-
-  if (inner) {
-    clipArc(ctx, element, startAngle + TAU);
-  }
-
-  ctx.beginPath();
-  ctx.arc(x, y, innerRadius, startAngle + TAU, startAngle, true);
-  for (i = 0; i < fullCircles; ++i) {
-    ctx.stroke();
-  }
-
-  ctx.beginPath();
-  ctx.arc(x, y, outerRadius, startAngle, startAngle + TAU);
-  for (i = 0; i < fullCircles; ++i) {
-    ctx.stroke();
-  }
-}
-
 function drawBorder(
   ctx: CanvasRenderingContext2D,
   element: ArcElement,
   offset: number,
   spacing: number,
-  endAngle: number,
   circular: boolean,
 ) {
-  const {options} = element;
+  const {fullCircles, startAngle, circumference, options} = element;
   const {borderWidth, borderJoinStyle} = options;
   const inner = options.borderAlign === 'inner';
 
@@ -257,16 +228,25 @@ function drawBorder(
     ctx.lineJoin = borderJoinStyle || 'bevel';
   }
 
-  if (element.fullCircles) {
-    drawFullCircleBorders(ctx, element, inner);
+  let endAngle = element.endAngle;
+  if (fullCircles) {
+    pathArc(ctx, element, offset, spacing, endAngle, circular);
+    for (let i = 0; i < fullCircles; ++i) {
+      ctx.stroke();
+    }
+    if (!isNaN(circumference)) {
+      endAngle = startAngle + (circumference % TAU || TAU);
+    }
   }
 
   if (inner) {
     clipArc(ctx, element, endAngle);
   }
 
-  pathArc(ctx, element, offset, spacing, endAngle, circular);
-  ctx.stroke();
+  if (!fullCircles) {
+    pathArc(ctx, element, offset, spacing, endAngle, circular);
+    ctx.stroke();
+  }
 }
 
 export interface ArcProps extends Point {
@@ -385,8 +365,8 @@ export default class ArcElement extends Element<ArcProps, ArcOptions> {
     ctx.fillStyle = options.backgroundColor;
     ctx.strokeStyle = options.borderColor;
 
-    const endAngle = drawArc(ctx, this, radiusOffset, spacing, circular);
-    drawBorder(ctx, this, radiusOffset, spacing, endAngle, circular);
+    drawArc(ctx, this, radiusOffset, spacing, circular);
+    drawBorder(ctx, this, radiusOffset, spacing, circular);
 
     ctx.restore();
   }
index 0591cca94251e29fa477a4b7e9de1356532aeade..db4e25235095a759549bb08ec0e1945d671644c5 100644 (file)
Binary files a/test/fixtures/controller.doughnut/single-slice-circumference-405.png and b/test/fixtures/controller.doughnut/single-slice-circumference-405.png differ
diff --git a/test/fixtures/controller.doughnut/single-slice-offset.js b/test/fixtures/controller.doughnut/single-slice-offset.js
new file mode 100644 (file)
index 0000000..d2a9ace
--- /dev/null
@@ -0,0 +1,16 @@
+module.exports = {
+  config: {
+    type: 'doughnut',
+    data: {
+      labels: ['A'],
+      datasets: [{
+        data: [385],
+        backgroundColor: 'rgba(0,0,0,0.3)',
+        borderColor: 'rgba(0,0,0,0.5)',
+      }]
+    },
+    options: {
+      offset: 20
+    }
+  }
+};
diff --git a/test/fixtures/controller.doughnut/single-slice-offset.png b/test/fixtures/controller.doughnut/single-slice-offset.png
new file mode 100644 (file)
index 0000000..b38c18b
Binary files /dev/null and b/test/fixtures/controller.doughnut/single-slice-offset.png differ