fix: Always draw full arcs and borders for doughnut slices (#10806)

* 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
This commit is contained in:
Richard Gibson 2022-11-12 14:47:08 -05:00 committed by GitHub
parent 69175847ef
commit 89487501b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 45 additions and 49 deletions

View 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();
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -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
}
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB