From e1868454ff2866417deef9910052e5d9585cbaf2 Mon Sep 17 00:00:00 2001 From: "v.terekhov" Date: Mon, 6 Jun 2022 16:02:45 +0400 Subject: [PATCH 1/7] feat: add circular prop to arc element draw actions --- src/core/core.datasetController.js | 6 +- src/elements/element.arc.js | 94 +++++++++++++++++------------- 2 files changed, 59 insertions(+), 41 deletions(-) diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index 85cc50e9162..2d96983f7ea 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -674,6 +674,8 @@ export default class DatasetController { const ctx = this._ctx; const chart = this.chart; const meta = this._cachedMeta; + const scale = meta.rScale || {}; + const scaleOptions = scale.options || {}; const elements = meta.data || []; const area = chart.chartArea; const active = []; @@ -694,12 +696,12 @@ export default class DatasetController { if (element.active && drawActiveElementsOnTop) { active.push(element); } else { - element.draw(ctx, area); + element.draw(ctx, area, scaleOptions); } } for (i = 0; i < active.length; ++i) { - active[i].draw(ctx, area); + active[i].draw(ctx, area, scaleOptions); } } diff --git a/src/elements/element.arc.js b/src/elements/element.arc.js index 1e03f9e1a7b..7ef4339a1cf 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, scaleOptions) { const {x, y, startAngle: start, pixelMargin, innerRadius: innerR} = element; const outerRadius = Math.max(element.outerRadius + spacing + offset - pixelMargin, 0); @@ -129,54 +129,71 @@ function pathArc(ctx, element, offset, spacing, end) { const innerStartAdjustedAngle = startAngle + innerStart / innerStartAdjustedRadius; const innerEndAdjustedAngle = endAngle - innerEnd / innerEndAdjustedRadius; + let circular = true; + if (scaleOptions !== undefined && scaleOptions.grid !== undefined && scaleOptions.grid.circular === false) { + circular = false; + } + 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 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); - // The line from point 7 to point 8 - const p8 = rThetaToXY(outerStartAdjustedRadius, startAngle, x, y); - ctx.lineTo(p8.x, p8.y); + const outerStartX = Math.cos(outerStartAdjustedAngle) * outerRadius + x; + const outerStartY = Math.sin(outerStartAdjustedAngle) * outerRadius + y; + ctx.lineTo(outerStartX, outerStartY); - // 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); + 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, scaleOptions) { 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, scaleOptions); for (let i = 0; i < fullCircles; ++i) { ctx.fill(); @@ -189,8 +206,7 @@ function drawArc(ctx, element, offset, spacing) { } } } - - pathArc(ctx, element, offset, spacing, endAngle); + pathArc(ctx, element, offset, spacing, endAngle, scaleOptions); ctx.fill(); return endAngle; } @@ -219,7 +235,7 @@ function drawFullCircleBorders(ctx, element, inner) { } } -function drawBorder(ctx, element, offset, spacing, endAngle) { +function drawBorder(ctx, element, offset, spacing, endAngle, scaleOptions) { const {options} = element; const {borderWidth, borderJoinStyle} = options; const inner = options.borderAlign === 'inner'; @@ -244,7 +260,7 @@ function drawBorder(ctx, element, offset, spacing, endAngle) { clipArc(ctx, element, endAngle); } - pathArc(ctx, element, offset, spacing, endAngle); + pathArc(ctx, element, offset, spacing, endAngle, scaleOptions); ctx.stroke(); } @@ -319,7 +335,7 @@ export default class ArcElement extends Element { return this.getCenterPoint(useFinalPosition); } - draw(ctx) { + draw(ctx, _area, scaleOptions) { const {options, circumference} = this; const offset = (options.offset || 0) / 2; const spacing = (options.spacing || 0) / 2; @@ -345,8 +361,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, scaleOptions); + drawBorder(ctx, this, radiusOffset, spacing, endAngle, scaleOptions); ctx.restore(); } From b58ae39f7d42e098efc390399332f65cfd7776db Mon Sep 17 00:00:00 2001 From: "v.terekhov" Date: Tue, 7 Jun 2022 12:39:08 +0400 Subject: [PATCH 2/7] test: add test for arc element with circular:false prop --- test/specs/element.arc.tests.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/specs/element.arc.tests.js b/test/specs/element.arc.tests.js index e2ec0788b4b..766920def60 100644 --- a/test/specs/element.arc.tests.js +++ b/test/specs/element.arc.tests.js @@ -218,4 +218,31 @@ 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, + }, + }, + } + } + }); + + var ctx = window.createMockContext(); + arc.draw(ctx); + + expect(ctx.getCalls().length).toBeGreaterThan(0); + }); }); From 6cfa6d53bf90a14ac811d77b0008dfa0d8106420 Mon Sep 17 00:00:00 2001 From: "v.terekhov" Date: Wed, 8 Jun 2022 13:44:35 +0400 Subject: [PATCH 3/7] feat: add circular prop to Arc element options --- src/core/core.datasetController.js | 6 ++---- src/elements/element.arc.js | 25 +++++++++++-------------- test/specs/element.arc.tests.js | 7 ++++++- types/index.esm.d.ts | 6 ++++++ 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index 2d96983f7ea..85cc50e9162 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -674,8 +674,6 @@ export default class DatasetController { const ctx = this._ctx; const chart = this.chart; const meta = this._cachedMeta; - const scale = meta.rScale || {}; - const scaleOptions = scale.options || {}; const elements = meta.data || []; const area = chart.chartArea; const active = []; @@ -696,12 +694,12 @@ export default class DatasetController { if (element.active && drawActiveElementsOnTop) { active.push(element); } else { - element.draw(ctx, area, scaleOptions); + element.draw(ctx, area); } } for (i = 0; i < active.length; ++i) { - active[i].draw(ctx, area, scaleOptions); + active[i].draw(ctx, area); } } diff --git a/src/elements/element.arc.js b/src/elements/element.arc.js index 7ef4339a1cf..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, scaleOptions) { +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); @@ -129,11 +129,6 @@ function pathArc(ctx, element, offset, spacing, end, scaleOptions) { const innerStartAdjustedAngle = startAngle + innerStart / innerStartAdjustedRadius; const innerEndAdjustedAngle = endAngle - innerEnd / innerEndAdjustedRadius; - let circular = true; - if (scaleOptions !== undefined && scaleOptions.grid !== undefined && scaleOptions.grid.circular === false) { - circular = false; - } - ctx.beginPath(); if (circular) { @@ -189,11 +184,11 @@ function pathArc(ctx, element, offset, spacing, end, scaleOptions) { ctx.closePath(); } -function drawArc(ctx, element, offset, spacing, scaleOptions) { +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, scaleOptions); + pathArc(ctx, element, offset, spacing, startAngle + TAU, circular); for (let i = 0; i < fullCircles; ++i) { ctx.fill(); @@ -206,7 +201,7 @@ function drawArc(ctx, element, offset, spacing, scaleOptions) { } } } - pathArc(ctx, element, offset, spacing, endAngle, scaleOptions); + pathArc(ctx, element, offset, spacing, endAngle, circular); ctx.fill(); return endAngle; } @@ -235,7 +230,7 @@ function drawFullCircleBorders(ctx, element, inner) { } } -function drawBorder(ctx, element, offset, spacing, endAngle, scaleOptions) { +function drawBorder(ctx, element, offset, spacing, endAngle, circular) { const {options} = element; const {borderWidth, borderJoinStyle} = options; const inner = options.borderAlign === 'inner'; @@ -260,7 +255,7 @@ function drawBorder(ctx, element, offset, spacing, endAngle, scaleOptions) { clipArc(ctx, element, endAngle); } - pathArc(ctx, element, offset, spacing, endAngle, scaleOptions); + pathArc(ctx, element, offset, spacing, endAngle, circular); ctx.stroke(); } @@ -335,10 +330,11 @@ export default class ArcElement extends Element { return this.getCenterPoint(useFinalPosition); } - draw(ctx, _area, scaleOptions) { + draw(ctx) { 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; @@ -361,8 +357,8 @@ export default class ArcElement extends Element { ctx.fillStyle = options.backgroundColor; ctx.strokeStyle = options.borderColor; - const endAngle = drawArc(ctx, this, radiusOffset, spacing, scaleOptions); - drawBorder(ctx, this, radiusOffset, spacing, endAngle, scaleOptions); + const endAngle = drawArc(ctx, this, radiusOffset, spacing, circular); + drawBorder(ctx, this, radiusOffset, spacing, endAngle, circular); ctx.restore(); } @@ -382,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 766920def60..fdfddab9315 100644 --- a/test/specs/element.arc.tests.js +++ b/test/specs/element.arc.tests.js @@ -236,7 +236,12 @@ describe('Arc element tests', function() { circular: false, }, }, - } + }, + elements: { + arc: { + circular: false + }, + }, } }); diff --git a/types/index.esm.d.ts b/types/index.esm.d.ts index adbdca76587..6c4d53f706d 100644 --- a/types/index.esm.d.ts +++ b/types/index.esm.d.ts @@ -1740,6 +1740,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 { From 3959a86180573455ea0ffa0f379894022e32fdc4 Mon Sep 17 00:00:00 2001 From: "v.terekhov" Date: Wed, 8 Jun 2022 14:44:34 +0400 Subject: [PATCH 4/7] docs: add decriptiption for new Polar area chart prop --- docs/charts/polar.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/charts/polar.md b/docs/charts/polar.md index 6c27700a4d2..1ec3eee9fed 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`](#general) | `boolean` | - | - | `true` All these values, if `undefined`, fallback to the scopes described in [option resolution](../general/options) @@ -74,6 +75,7 @@ All these values, if `undefined`, fallback to the scopes described in [option re | Name | Description | ---- | ---- | `clip` | How to clip relative to chartArea. Positive value allows overflow, negative value clips that many pixels inside chartArea. `0` = clip at chartArea. Clipping can also be configured per side: `clip: {left: 5, top: false, right: -2, bottom: 0}` +| `circular` | By default Arc is curve. If `circular: false` Arc will be flat. ### Styling From e26091ce8c4351f5e7db241b728c59db401567e7 Mon Sep 17 00:00:00 2001 From: "v.terekhov" Date: Wed, 8 Jun 2022 16:28:57 +0400 Subject: [PATCH 5/7] docs: fix circular prop description --- docs/charts/polar.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/charts/polar.md b/docs/charts/polar.md index 1ec3eee9fed..d8b61835cac 100644 --- a/docs/charts/polar.md +++ b/docs/charts/polar.md @@ -66,7 +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`](#general) | `boolean` | - | - | `true` +| [`circular`](#general) | `boolean` | Yes | Yes | `true` All these values, if `undefined`, fallback to the scopes described in [option resolution](../general/options) @@ -75,7 +75,7 @@ All these values, if `undefined`, fallback to the scopes described in [option re | Name | Description | ---- | ---- | `clip` | How to clip relative to chartArea. Positive value allows overflow, negative value clips that many pixels inside chartArea. `0` = clip at chartArea. Clipping can also be configured per side: `clip: {left: 5, top: false, right: -2, bottom: 0}` -| `circular` | By default Arc is curve. If `circular: false` Arc will be flat. +| `circular` | By default the Arc is curved. If `circular: false` the Arc will be flat. ### Styling From 6721cd5ea2cd289c19269d83e2529120b65d7fad Mon Sep 17 00:00:00 2001 From: "v.terekhov" Date: Thu, 9 Jun 2022 11:51:47 +0400 Subject: [PATCH 6/7] docs: add info about arc element circular prop to elements docs --- docs/configuration/elements.md | 1 + 1 file changed, 1 insertion(+) 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 From 4b15031e87cc876c19ed8db79169566f5ba9a36c Mon Sep 17 00:00:00 2001 From: "v.terekhov" Date: Thu, 9 Jun 2022 13:26:15 +0400 Subject: [PATCH 7/7] docs: move circular prop from general options to styling --- docs/charts/polar.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/charts/polar.md b/docs/charts/polar.md index d8b61835cac..59f1437cd5d 100644 --- a/docs/charts/polar.md +++ b/docs/charts/polar.md @@ -66,7 +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`](#general) | `boolean` | Yes | Yes | `true` +| [`circular`](#styling) | `boolean` | Yes | Yes | `true` All these values, if `undefined`, fallback to the scopes described in [option resolution](../general/options) @@ -75,7 +75,6 @@ All these values, if `undefined`, fallback to the scopes described in [option re | Name | Description | ---- | ---- | `clip` | How to clip relative to chartArea. Positive value allows overflow, negative value clips that many pixels inside chartArea. `0` = clip at chartArea. Clipping can also be configured per side: `clip: {left: 5, top: false, right: -2, bottom: 0}` -| `circular` | By default the Arc is curved. If `circular: false` the Arc will be flat. ### Styling @@ -87,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.