From: Pierre <73789471+R2Jeu-prive@users.noreply.github.com> Date: Tue, 15 Apr 2025 13:32:13 +0000 (+0200) Subject: Add selfJoin option for doughnut graphs (#12054) X-Git-Tag: v4.5.0~11 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=74961d1c868e47429fc1206679f0c9ba09b75ab5;p=thirdparty%2FChart.js.git Add selfJoin option for doughnut graphs (#12054) Co-authored-by: Pierre Gueguen --- diff --git a/src/elements/element.arc.ts b/src/elements/element.arc.ts index e2bd26f52..42f41f045 100644 --- a/src/elements/element.arc.ts +++ b/src/elements/element.arc.ts @@ -1,9 +1,42 @@ import Element from '../core/core.element.js'; import {_angleBetween, getAngleFromPoint, TAU, HALF_PI, valueOrDefault} from '../helpers/index.js'; -import {PI, _isBetween, _limitValue} from '../helpers/helpers.math.js'; +import {PI, _angleDiff, _normalizeAngle, _isBetween, _limitValue} from '../helpers/helpers.math.js'; import {_readValueToProps} from '../helpers/helpers.options.js'; import type {ArcOptions, Point} from '../types/index.js'; +function clipSelf(ctx: CanvasRenderingContext2D, element: ArcElement, endAngle: number) { + const {startAngle, x, y, outerRadius, innerRadius, options} = element; + const {borderWidth, borderJoinStyle} = options; + const outerAngleClip = Math.min(borderWidth / outerRadius, _normalizeAngle(startAngle - endAngle)); + ctx.beginPath(); + ctx.arc(x, y, outerRadius - borderWidth / 2, startAngle + outerAngleClip / 2, endAngle - outerAngleClip / 2); + + if (innerRadius > 0) { + const innerAngleClip = Math.min(borderWidth / innerRadius, _normalizeAngle(startAngle - endAngle)); + ctx.arc(x, y, innerRadius + borderWidth / 2, endAngle - innerAngleClip / 2, startAngle + innerAngleClip / 2, true); + } else { + const clipWidth = Math.min(borderWidth / 2, outerRadius * _normalizeAngle(startAngle - endAngle)); + + if (borderJoinStyle === 'round') { + ctx.arc(x, y, clipWidth, endAngle - PI / 2, startAngle + PI / 2, true); + } else if (borderJoinStyle === 'bevel') { + const r = 2 * clipWidth * clipWidth; + const endX = -r * Math.cos(endAngle + PI / 2) + x; + const endY = -r * Math.sin(endAngle + PI / 2) + y; + const startX = r * Math.cos(startAngle + PI / 2) + x; + const startY = r * Math.sin(startAngle + PI / 2) + y; + ctx.lineTo(endX, endY); + ctx.lineTo(startX, startY); + } + } + ctx.closePath(); + + ctx.moveTo(0, 0); + ctx.rect(0, 0, ctx.canvas.width, ctx.canvas.height); + + ctx.clip('evenodd'); +} + function clipArc(ctx: CanvasRenderingContext2D, element: ArcElement, endAngle: number) { const {startAngle, pixelMargin, x, y, outerRadius, innerRadius} = element; @@ -213,7 +246,7 @@ function drawBorder( circular: boolean, ) { const {fullCircles, startAngle, circumference, options} = element; - const {borderWidth, borderJoinStyle, borderDash, borderDashOffset} = options; + const {borderWidth, borderJoinStyle, borderDash, borderDashOffset, borderRadius} = options; const inner = options.borderAlign === 'inner'; if (!borderWidth) { @@ -246,6 +279,10 @@ function drawBorder( clipArc(ctx, element, endAngle); } + if (options.selfJoin && endAngle - startAngle >= PI && borderRadius === 0 && borderJoinStyle !== 'miter') { + clipSelf(ctx, element, endAngle); + } + if (!fullCircles) { pathArc(ctx, element, offset, spacing, endAngle, circular); ctx.stroke(); @@ -276,6 +313,7 @@ export default class ArcElement extends Element { spacing: 0, angle: undefined, circular: true, + selfJoin: false, }; static defaultRoutes = { diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 807fe8208..175336ec7 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -1847,6 +1847,12 @@ export interface ArcBorderRadius { } export interface ArcOptions extends CommonElementOptions { + /** + * If true, Arc can take up 100% of a circular graph without any visual split or cut. This option doesn't support borderRadius and borderJoinStyle miter + * @default true + */ + selfJoin: boolean; + /** * Arc stroke alignment. */ diff --git a/test/fixtures/controller.doughnut/selfJoin/doughnut.js b/test/fixtures/controller.doughnut/selfJoin/doughnut.js new file mode 100644 index 000000000..f29939cec --- /dev/null +++ b/test/fixtures/controller.doughnut/selfJoin/doughnut.js @@ -0,0 +1,25 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: ['Red'], + datasets: [ + { + // option in dataset + data: [100], + borderWidth: 15, + backgroundColor: '#FF0000', + borderColor: '#000000', + borderAlign: 'center', + selfJoin: true + } + ] + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.doughnut/selfJoin/doughnut.png b/test/fixtures/controller.doughnut/selfJoin/doughnut.png new file mode 100644 index 000000000..af2d4b438 Binary files /dev/null and b/test/fixtures/controller.doughnut/selfJoin/doughnut.png differ diff --git a/test/fixtures/controller.doughnut/selfJoin/pie.js b/test/fixtures/controller.doughnut/selfJoin/pie.js new file mode 100644 index 000000000..d0187db09 --- /dev/null +++ b/test/fixtures/controller.doughnut/selfJoin/pie.js @@ -0,0 +1,26 @@ +module.exports = { + config: { + type: 'pie', + data: { + labels: ['Red'], + datasets: [ + { + // option in dataset + data: [100], + borderWidth: 15, + backgroundColor: '#FF0000', + borderColor: '#000000', + borderAlign: 'center', + borderJoinStyle: 'round', + selfJoin: true + } + ] + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.doughnut/selfJoin/pie.png b/test/fixtures/controller.doughnut/selfJoin/pie.png new file mode 100644 index 000000000..17a2e3b19 Binary files /dev/null and b/test/fixtures/controller.doughnut/selfJoin/pie.png differ