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 * Path the arc, respecting border radius by separating into left and right halves.
*
* 8 points of interest exist around the arc segment.
* These points define the intersection of the arc edges and the corners.
* *
* Start End * Start End
* *
* 1---------2 Outer * 1--->a--->2 Outer
* / \ * / \
* 8 3 * 8 3
* | | * | |
* | | * | |
* 7 4 * 7 4
* \ / * \ /
* 6---------5 Inner * 6<---b<---5 Inner
*/ */
function pathArc( function pathArc(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
@ -129,8 +126,10 @@ function pathArc(
ctx.beginPath(); ctx.beginPath();
if (circular) { if (circular) {
// The first arc segment from point 1 to point 2 // The first arc segments from point 1 to point a to point 2
ctx.arc(x, y, outerRadius, outerStartAdjustedAngle, outerEndAdjustedAngle); 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 // The corner segment from point 2 to point 3
if (outerEnd > 0) { if (outerEnd > 0) {
@ -148,8 +147,10 @@ function pathArc(
ctx.arc(pCenter.x, pCenter.y, innerEnd, endAngle + HALF_PI, innerEndAdjustedAngle + Math.PI); ctx.arc(pCenter.x, pCenter.y, innerEnd, endAngle + HALF_PI, innerEndAdjustedAngle + Math.PI);
} }
// The inner arc from point 5 to point 6 // The inner arc from point 5 to point b to point 6
ctx.arc(x, y, innerRadius, endAngle - (innerEnd / innerRadius), startAngle + (innerStart / innerRadius), true); 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 // The corner segment from point 6 to point 7
if (innerStart > 0) { if (innerStart > 0) {
@ -191,17 +192,12 @@ function drawArc(
const {fullCircles, startAngle, circumference} = element; const {fullCircles, startAngle, circumference} = element;
let endAngle = element.endAngle; let endAngle = element.endAngle;
if (fullCircles) { if (fullCircles) {
pathArc(ctx, element, offset, spacing, startAngle + TAU, circular); pathArc(ctx, element, offset, spacing, endAngle, circular);
for (let i = 0; i < fullCircles; ++i) { for (let i = 0; i < fullCircles; ++i) {
ctx.fill(); ctx.fill();
} }
if (!isNaN(circumference)) { if (!isNaN(circumference)) {
endAngle = startAngle + circumference % TAU; endAngle = startAngle + (circumference % TAU || TAU);
if (circumference % TAU === 0) {
endAngle += TAU;
}
} }
} }
pathArc(ctx, element, offset, spacing, endAngle, circular); pathArc(ctx, element, offset, spacing, endAngle, circular);
@ -209,39 +205,14 @@ function drawArc(
return endAngle; 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( function drawBorder(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
element: ArcElement, element: ArcElement,
offset: number, offset: number,
spacing: number, spacing: number,
endAngle: number,
circular: boolean, circular: boolean,
) { ) {
const {options} = element; const {fullCircles, startAngle, circumference, options} = element;
const {borderWidth, borderJoinStyle} = options; const {borderWidth, borderJoinStyle} = options;
const inner = options.borderAlign === 'inner'; const inner = options.borderAlign === 'inner';
@ -257,16 +228,25 @@ function drawBorder(
ctx.lineJoin = borderJoinStyle || 'bevel'; ctx.lineJoin = borderJoinStyle || 'bevel';
} }
if (element.fullCircles) { let endAngle = element.endAngle;
drawFullCircleBorders(ctx, element, inner); 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) { if (inner) {
clipArc(ctx, element, endAngle); clipArc(ctx, element, endAngle);
} }
pathArc(ctx, element, offset, spacing, endAngle, circular); if (!fullCircles) {
ctx.stroke(); pathArc(ctx, element, offset, spacing, endAngle, circular);
ctx.stroke();
}
} }
export interface ArcProps extends Point { export interface ArcProps extends Point {
@ -385,8 +365,8 @@ export default class ArcElement extends Element<ArcProps, ArcOptions> {
ctx.fillStyle = options.backgroundColor; ctx.fillStyle = options.backgroundColor;
ctx.strokeStyle = options.borderColor; ctx.strokeStyle = options.borderColor;
const endAngle = drawArc(ctx, this, radiusOffset, spacing, circular); drawArc(ctx, this, radiusOffset, spacing, circular);
drawBorder(ctx, this, radiusOffset, spacing, endAngle, circular); drawBorder(ctx, this, radiusOffset, spacing, circular);
ctx.restore(); 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