From: Richard Gibson Date: Sat, 12 Nov 2022 19:47:08 +0000 (-0500) Subject: fix: Always draw full arcs and borders for doughnut slices (#10806) X-Git-Tag: v4.0.0-release~3 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=89487501b6d05502da0fd57ee6233bb35d07716c;p=thirdparty%2FChart.js.git 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 --- diff --git a/src/elements/element.arc.ts b/src/elements/element.arc.ts index b9b9474e7..7e19c9115 100644 --- a/src/elements/element.arc.ts +++ b/src/elements/element.arc.ts @@ -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 { 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(); } diff --git a/test/fixtures/controller.doughnut/single-slice-circumference-405.png b/test/fixtures/controller.doughnut/single-slice-circumference-405.png index 0591cca94..db4e25235 100644 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 index 000000000..d2a9ace0c --- /dev/null +++ b/test/fixtures/controller.doughnut/single-slice-offset.js @@ -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 index 000000000..b38c18b80 Binary files /dev/null and b/test/fixtures/controller.doughnut/single-slice-offset.png differ