From f0be17c85993aff79db293d6bdf5c7e07f90169f Mon Sep 17 00:00:00 2001 From: Slava Terekhov Date: Sat, 30 Jul 2022 19:08:38 +0400 Subject: [PATCH] Add circular prop to arc element (#10405) * feat: add circular prop to arc element draw actions * test: add test for arc element with circular:false prop * feat: add circular prop to Arc element options * docs: add decriptiption for new Polar area chart prop * docs: fix circular prop description * docs: add info about arc element circular prop to elements docs * docs: move circular prop from general options to styling --- docs/charts/polar.md | 2 + docs/configuration/elements.md | 1 + src/elements/element.arc.js | 89 +++++++++++++++++++-------------- test/specs/element.arc.tests.js | 32 ++++++++++++ types/index.esm.d.ts | 6 +++ 5 files changed, 92 insertions(+), 38 deletions(-) diff --git a/docs/charts/polar.md b/docs/charts/polar.md index 5870dfd2f63..0f9c8fcee1b 100644 --- a/docs/charts/polar.md +++ b/docs/charts/polar.md @@ -66,6 +66,7 @@ The following options can be included in a polar area chart dataset to configure | [`hoverBorderColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | `undefined` | [`hoverBorderJoinStyle`](#interactions) | `'round'`\|`'bevel'`\|`'miter'` | Yes | Yes | `undefined` | [`hoverBorderWidth`](#interactions) | `number` | Yes | Yes | `undefined` +| [`circular`](#styling) | `boolean` | Yes | Yes | `true` All these values, if `undefined`, fallback to the scopes described in [option resolution](../general/options) @@ -85,6 +86,7 @@ The style of each arc can be controlled with the following properties: | `borderColor` | arc border color. | `borderJoinStyle` | arc border join style. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin). | `borderWidth` | arc border width (in pixels). +| `circular` | By default the Arc is curved. If `circular: false` the Arc will be flat. All these values, if `undefined`, fallback to the associated [`elements.arc.*`](../configuration/elements.md#arc-configuration) options. diff --git a/docs/configuration/elements.md b/docs/configuration/elements.md index ec5fe9659d0..e37a12e27ec 100644 --- a/docs/configuration/elements.md +++ b/docs/configuration/elements.md @@ -101,3 +101,4 @@ Namespace: `options.elements.arc`, global arc options: `Chart.defaults.elements. | `borderColor` | [`Color`](/general/colors.md) | `'#fff'` | Arc stroke color. | `borderJoinStyle` | `'round'`\|`'bevel'`\|`'miter'` | `'bevel'`\|`'round'` | Line join style. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin). The default is `'round'` when `borderAlign` is `'inner'` | `borderWidth`| `number` | `2` | Arc stroke width. +| `circular` | `boolean` | `true` | By default the Arc is curved. If `circular: false` the Arc will be flat diff --git a/src/elements/element.arc.js b/src/elements/element.arc.js index 1e03f9e1a7b..3ec9091b77f 100644 --- a/src/elements/element.arc.js +++ b/src/elements/element.arc.js @@ -93,7 +93,7 @@ function rThetaToXY(r, theta, x, y) { * @param {CanvasRenderingContext2D} ctx * @param {ArcElement} element */ -function pathArc(ctx, element, offset, spacing, end) { +function pathArc(ctx, element, offset, spacing, end, circular) { const {x, y, startAngle: start, pixelMargin, innerRadius: innerR} = element; const outerRadius = Math.max(element.outerRadius + spacing + offset - pixelMargin, 0); @@ -131,52 +131,64 @@ function pathArc(ctx, element, offset, spacing, end) { ctx.beginPath(); - // The first arc segment from point 1 to point 2 - ctx.arc(x, y, outerRadius, outerStartAdjustedAngle, outerEndAdjustedAngle); + if (circular) { + // The first arc segment from point 1 to point 2 + ctx.arc(x, y, outerRadius, outerStartAdjustedAngle, outerEndAdjustedAngle); - // The corner segment from point 2 to point 3 - if (outerEnd > 0) { - const pCenter = rThetaToXY(outerEndAdjustedRadius, outerEndAdjustedAngle, x, y); - ctx.arc(pCenter.x, pCenter.y, outerEnd, outerEndAdjustedAngle, endAngle + HALF_PI); - } + // The corner segment from point 2 to point 3 + if (outerEnd > 0) { + const pCenter = rThetaToXY(outerEndAdjustedRadius, outerEndAdjustedAngle, x, y); + ctx.arc(pCenter.x, pCenter.y, outerEnd, outerEndAdjustedAngle, endAngle + HALF_PI); + } - // The line from point 3 to point 4 - const p4 = rThetaToXY(innerEndAdjustedRadius, endAngle, x, y); - ctx.lineTo(p4.x, p4.y); + // The line from point 3 to point 4 + const p4 = rThetaToXY(innerEndAdjustedRadius, endAngle, x, y); + ctx.lineTo(p4.x, p4.y); - // The corner segment from point 4 to point 5 - if (innerEnd > 0) { - const pCenter = rThetaToXY(innerEndAdjustedRadius, innerEndAdjustedAngle, x, y); - ctx.arc(pCenter.x, pCenter.y, innerEnd, endAngle + HALF_PI, innerEndAdjustedAngle + Math.PI); - } + // The corner segment from point 4 to point 5 + if (innerEnd > 0) { + const pCenter = rThetaToXY(innerEndAdjustedRadius, innerEndAdjustedAngle, x, y); + 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 6 + ctx.arc(x, y, innerRadius, endAngle - (innerEnd / innerRadius), startAngle + (innerStart / innerRadius), true); - // The corner segment from point 6 to point 7 - if (innerStart > 0) { - const pCenter = rThetaToXY(innerStartAdjustedRadius, innerStartAdjustedAngle, x, y); - ctx.arc(pCenter.x, pCenter.y, innerStart, innerStartAdjustedAngle + Math.PI, startAngle - HALF_PI); - } + // The corner segment from point 6 to point 7 + if (innerStart > 0) { + const pCenter = rThetaToXY(innerStartAdjustedRadius, innerStartAdjustedAngle, x, y); + ctx.arc(pCenter.x, pCenter.y, innerStart, innerStartAdjustedAngle + Math.PI, startAngle - HALF_PI); + } - // The line from point 7 to point 8 - const p8 = rThetaToXY(outerStartAdjustedRadius, startAngle, x, y); - ctx.lineTo(p8.x, p8.y); + // The line from point 7 to point 8 + const p8 = rThetaToXY(outerStartAdjustedRadius, startAngle, x, y); + ctx.lineTo(p8.x, p8.y); - // The corner segment from point 8 to point 1 - if (outerStart > 0) { - const pCenter = rThetaToXY(outerStartAdjustedRadius, outerStartAdjustedAngle, x, y); - ctx.arc(pCenter.x, pCenter.y, outerStart, startAngle - HALF_PI, outerStartAdjustedAngle); + // The corner segment from point 8 to point 1 + if (outerStart > 0) { + const pCenter = rThetaToXY(outerStartAdjustedRadius, outerStartAdjustedAngle, x, y); + ctx.arc(pCenter.x, pCenter.y, outerStart, startAngle - HALF_PI, outerStartAdjustedAngle); + } + } else { + ctx.moveTo(x, y); + + const outerStartX = Math.cos(outerStartAdjustedAngle) * outerRadius + x; + const outerStartY = Math.sin(outerStartAdjustedAngle) * outerRadius + y; + ctx.lineTo(outerStartX, outerStartY); + + const outerEndX = Math.cos(outerEndAdjustedAngle) * outerRadius + x; + const outerEndY = Math.sin(outerEndAdjustedAngle) * outerRadius + y; + ctx.lineTo(outerEndX, outerEndY); } ctx.closePath(); } -function drawArc(ctx, element, offset, spacing) { +function drawArc(ctx, element, offset, spacing, circular) { const {fullCircles, startAngle, circumference} = element; let endAngle = element.endAngle; if (fullCircles) { - pathArc(ctx, element, offset, spacing, startAngle + TAU); + pathArc(ctx, element, offset, spacing, startAngle + TAU, circular); for (let i = 0; i < fullCircles; ++i) { ctx.fill(); @@ -189,8 +201,7 @@ function drawArc(ctx, element, offset, spacing) { } } } - - pathArc(ctx, element, offset, spacing, endAngle); + pathArc(ctx, element, offset, spacing, endAngle, circular); ctx.fill(); return endAngle; } @@ -219,7 +230,7 @@ function drawFullCircleBorders(ctx, element, inner) { } } -function drawBorder(ctx, element, offset, spacing, endAngle) { +function drawBorder(ctx, element, offset, spacing, endAngle, circular) { const {options} = element; const {borderWidth, borderJoinStyle} = options; const inner = options.borderAlign === 'inner'; @@ -244,7 +255,7 @@ function drawBorder(ctx, element, offset, spacing, endAngle) { clipArc(ctx, element, endAngle); } - pathArc(ctx, element, offset, spacing, endAngle); + pathArc(ctx, element, offset, spacing, endAngle, circular); ctx.stroke(); } @@ -323,6 +334,7 @@ export default class ArcElement extends Element { const {options, circumference} = this; const offset = (options.offset || 0) / 2; const spacing = (options.spacing || 0) / 2; + const circular = options.circular; this.pixelMargin = (options.borderAlign === 'inner') ? 0.33 : 0; this.fullCircles = circumference > TAU ? Math.floor(circumference / TAU) : 0; @@ -345,8 +357,8 @@ export default class ArcElement extends Element { ctx.fillStyle = options.backgroundColor; ctx.strokeStyle = options.borderColor; - const endAngle = drawArc(ctx, this, radiusOffset, spacing); - drawBorder(ctx, this, radiusOffset, spacing, endAngle); + const endAngle = drawArc(ctx, this, radiusOffset, spacing, circular); + drawBorder(ctx, this, radiusOffset, spacing, endAngle, circular); ctx.restore(); } @@ -366,6 +378,7 @@ ArcElement.defaults = { offset: 0, spacing: 0, angle: undefined, + circular: true, }; /** diff --git a/test/specs/element.arc.tests.js b/test/specs/element.arc.tests.js index e2ec0788b4b..fdfddab9315 100644 --- a/test/specs/element.arc.tests.js +++ b/test/specs/element.arc.tests.js @@ -218,4 +218,36 @@ describe('Arc element tests', function() { expect(ctx.getCalls().length).toBe(0); }); + + it('should draw when circular: false', function() { + var arc = new Chart.elements.ArcElement({ + startAngle: 0, + endAngle: Math.PI * 2, + x: 2, + y: 2, + innerRadius: 0, + outerRadius: 2, + options: { + spacing: 0, + offset: 0, + scales: { + r: { + grid: { + circular: false, + }, + }, + }, + elements: { + arc: { + circular: false + }, + }, + } + }); + + var ctx = window.createMockContext(); + arc.draw(ctx); + + expect(ctx.getCalls().length).toBeGreaterThan(0); + }); }); diff --git a/types/index.esm.d.ts b/types/index.esm.d.ts index 75304c1f2e2..4284e2d1019 100644 --- a/types/index.esm.d.ts +++ b/types/index.esm.d.ts @@ -1746,6 +1746,12 @@ export interface ArcOptions extends CommonElementOptions { * Arc offset (in pixels). */ offset: number; + + /** + * If false, Arc will be flat. + * @default true + */ + circular: boolean; } export interface ArcHoverOptions extends CommonHoverOptions {