From: Jukka Kurkela Date: Wed, 5 Aug 2020 11:35:28 +0000 (+0300) Subject: Add offset option for arc (#7691) X-Git-Tag: v3.0.0-beta.2~17 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=f79d609548aebc6d70b16a51a80574827f051713;p=thirdparty%2FChart.js.git Add offset option for arc (#7691) * Add offset option for arc * Finishing touches --- diff --git a/docs/docs/charts/doughnut.mdx b/docs/docs/charts/doughnut.mdx index 346b6ecd1..ab1b15d69 100644 --- a/docs/docs/charts/doughnut.mdx +++ b/docs/docs/charts/doughnut.mdx @@ -10,9 +10,22 @@ Pie and doughnut charts are effectively the same class in Chart.js, but have one They are also registered under two aliases in the `Chart` core. Other than their different default value, and different alias, they are exactly the same. -import { useEffect } from 'react'; - -export const ExampleChart = () => { +import { useEffect, useRef } from 'react'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + + +```jsx live +function example() { + const canvas = useRef(null); useEffect(() => { const cfg = { type: 'doughnut', @@ -29,36 +42,53 @@ export const ExampleChart = () => { 'rgb(255, 99, 132)', 'rgb(54, 162, 235)', 'rgb(255, 205, 86)' - ] + ], + hoverOffset: 4 }] } }; - new Chart(document.getElementById('chartjs-0').getContext('2d'), cfg); + new Chart(canvas.current.getContext('2d'), cfg); }); - return
; + return
; } +``` - +
-## Example Usage + -```javascript -// For a pie chart -var myPieChart = new Chart(ctx, { - type: 'pie', - data: data, - options: options -}); +```jsx live +function example() { + const canvas = useRef(null); + useEffect(() => { + const cfg = { + type: 'pie', + data: { + labels: [ + 'Red', + 'Blue', + 'Yellow' + ], + datasets: [{ + label: 'My First Dataset', + data: [300, 50, 100], + backgroundColor: [ + 'rgb(255, 99, 132)', + 'rgb(54, 162, 235)', + 'rgb(255, 205, 86)' + ], + hoverOffset: 4 + }] + } + }; + new Chart(canvas.current.getContext('2d'), cfg); + }); + return
; +} ``` -```javascript -// And for a doughnut chart -var myDoughnutChart = new Chart(ctx, { - type: 'doughnut', - data: data, - options: options -}); -``` +
+
## Dataset Properties @@ -75,6 +105,8 @@ The doughnut/pie chart allows a number of properties to be specified for each da | [`hoverBackgroundColor`](#interations) | [`Color`](../general/colors.md) | Yes | Yes | `undefined` | [`hoverBorderColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | `undefined` | [`hoverBorderWidth`](#interactions) | `number` | Yes | Yes | `undefined` +| [`hoverOffset`](#interactions) | `number` | Yes | Yes | `0` +| [`offset`](#styling) | `number` | Yes | Yes | `0` | [`weight`](#styling) | `number` | - | - | `1` ### General @@ -93,6 +125,7 @@ The style of each arc can be controlled with the following properties: | `backgroundColor` | arc background color. | `borderColor` | arc border color. | `borderWidth` | arc border width (in pixels). +| `offset` | arc offset (in pixels). | `weight` | The relative thickness of the dataset. Providing a value for weight will cause the pie or doughnut dataset to be drawn with a thickness relative to the sum of all the dataset weight values. All these values, if `undefined`, fallback to the associated [`elements.arc.*`](../configuration/elements.md#arc-configuration) options. @@ -114,6 +147,7 @@ The interaction with each arc can be controlled with the following properties: | `hoverBackgroundColor` | arc background color when hovered. | `hoverBorderColor` | arc border color when hovered. | `hoverBorderWidth` | arc border width when hovered (in pixels). +| `hoverOffset` | arc offset when hovered (in pixels). All these values, if `undefined`, fallback to the associated [`elements.arc.*`](../configuration/elements.md#arc-configuration) options. diff --git a/src/controllers/controller.doughnut.js b/src/controllers/controller.doughnut.js index a5a107f4f..562ae11af 100644 --- a/src/controllers/controller.doughnut.js +++ b/src/controllers/controller.doughnut.js @@ -89,9 +89,9 @@ export default class DoughnutController extends DatasetController { const cutout = options.cutoutPercentage / 100 || 0; const chartWeight = me._getRingWeight(me.index); const {ratioX, ratioY, offsetX, offsetY} = getRatioAndOffset(options.rotation, options.circumference, cutout); - const borderWidth = me.getMaxBorderWidth(); - const maxWidth = (chartArea.right - chartArea.left - borderWidth) / ratioX; - const maxHeight = (chartArea.bottom - chartArea.top - borderWidth) / ratioY; + const spacing = me.getMaxBorderWidth() + me.getMaxOffset(arcs); + const maxWidth = (chartArea.right - chartArea.left - spacing) / ratioX; + const maxHeight = (chartArea.bottom - chartArea.top - spacing) / ratioY; const outerRadius = Math.max(Math.min(maxWidth, maxHeight) / 2, 0); const innerRadius = Math.max(outerRadius * cutout, 0); const radiusLength = (outerRadius - innerRadius) / me._getVisibleDatasetWeightTotal(); @@ -219,6 +219,16 @@ export default class DoughnutController extends DatasetController { return max; } + getMaxOffset(arcs) { + let max = 0; + + for (let i = 0, ilen = arcs.length; i < ilen; ++i) { + const options = this.resolveDataElementOptions(i); + max = Math.max(max, options.offset || 0, options.hoverOffset || 0); + } + return max; + } + /** * Get radius length offset of the dataset in relation to the visible datasets weights. This allows determining the inner and outer radius correctly * @private @@ -264,14 +274,12 @@ DoughnutController.defaults = { 'borderColor', 'borderWidth', 'borderAlign', - 'hoverBackgroundColor', - 'hoverBorderColor', - 'hoverBorderWidth', + 'offset' ], animation: { numbers: { type: 'number', - properties: ['circumference', 'endAngle', 'innerRadius', 'outerRadius', 'startAngle', 'x', 'y'] + properties: ['circumference', 'endAngle', 'innerRadius', 'outerRadius', 'startAngle', 'x', 'y', 'offset', 'borderWidth'] }, // Boolean - Whether we animate the rotation of the Doughnut animateRotate: true, diff --git a/src/controllers/controller.polarArea.js b/src/controllers/controller.polarArea.js index 06bc21698..60e5c042b 100644 --- a/src/controllers/controller.polarArea.js +++ b/src/controllers/controller.polarArea.js @@ -148,9 +148,7 @@ PolarAreaController.defaults = { 'borderColor', 'borderWidth', 'borderAlign', - 'hoverBackgroundColor', - 'hoverBorderColor', - 'hoverBorderWidth' + 'offset' ], animation: { diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index 9f9213650..f79efba87 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -714,7 +714,7 @@ export default class DatasetController { /** * @param {number} index - * @param {string} mode + * @param {string} [mode] * @protected */ resolveDataElementOptions(index, mode) { diff --git a/src/elements/element.arc.js b/src/elements/element.arc.js index a69415a03..810c9d7a3 100644 --- a/src/elements/element.arc.js +++ b/src/elements/element.arc.js @@ -3,17 +3,17 @@ import {_angleBetween, getAngleFromPoint} from '../helpers/helpers.math'; const TAU = Math.PI * 2; -function clipArc(ctx, model) { - const {startAngle, endAngle, pixelMargin, x, y} = model; - let angleMargin = pixelMargin / model.outerRadius; +function clipArc(ctx, element) { + const {startAngle, endAngle, pixelMargin, x, y, outerRadius, innerRadius} = element; + let angleMargin = pixelMargin / outerRadius; // Draw an inner border by cliping the arc and drawing a double-width border // Enlarge the clipping arc by 0.33 pixels to eliminate glitches between borders ctx.beginPath(); - ctx.arc(x, y, model.outerRadius, startAngle - angleMargin, endAngle + angleMargin); - if (model.innerRadius > pixelMargin) { - angleMargin = pixelMargin / model.innerRadius; - ctx.arc(x, y, model.innerRadius - pixelMargin, endAngle + angleMargin, startAngle - angleMargin, true); + ctx.arc(x, y, outerRadius, startAngle - angleMargin, endAngle + angleMargin); + if (innerRadius > pixelMargin) { + angleMargin = pixelMargin / innerRadius; + ctx.arc(x, y, innerRadius, endAngle + angleMargin, startAngle - angleMargin, true); } else { ctx.arc(x, y, pixelMargin, endAngle + Math.PI / 2, startAngle - Math.PI / 2); } @@ -22,60 +22,73 @@ function clipArc(ctx, model) { } -function pathArc(ctx, model) { +function pathArc(ctx, element) { + const {x, y, startAngle, endAngle, pixelMargin} = element; + const outerRadius = Math.max(element.outerRadius - pixelMargin, 0); + const innerRadius = element.innerRadius + pixelMargin; + ctx.beginPath(); - ctx.arc(model.x, model.y, model.outerRadius, model.startAngle, model.endAngle); - ctx.arc(model.x, model.y, model.innerRadius, model.endAngle, model.startAngle, true); + ctx.arc(x, y, outerRadius, startAngle, endAngle); + ctx.arc(x, y, innerRadius, endAngle, startAngle, true); ctx.closePath(); } -function drawArc(ctx, model, circumference) { - if (model.fullCircles) { - model.endAngle = model.startAngle + TAU; +function drawArc(ctx, element) { + if (element.fullCircles) { + element.endAngle = element.startAngle + TAU; - pathArc(ctx, model); + pathArc(ctx, element); - for (let i = 0; i < model.fullCircles; ++i) { + for (let i = 0; i < element.fullCircles; ++i) { ctx.fill(); } - model.endAngle = model.startAngle + circumference % TAU; + element.endAngle = element.startAngle + element.circumference % TAU; } - pathArc(ctx, model); + pathArc(ctx, element); ctx.fill(); } -function drawFullCircleBorders(ctx, element, model, inner) { - const endAngle = model.endAngle; +function drawFullCircleBorders(ctx, element, inner) { + const {x, y, startAngle, endAngle, pixelMargin} = element; + const outerRadius = Math.max(element.outerRadius - pixelMargin, 0); + const innerRadius = element.innerRadius + pixelMargin; + let i; if (inner) { - model.endAngle = model.startAngle + TAU; - clipArc(ctx, model); - model.endAngle = endAngle; - if (model.endAngle === model.startAngle && model.fullCircles) { - model.endAngle += TAU; - model.fullCircles--; + element.endAngle = element.startAngle + TAU; + clipArc(ctx, element); + element.endAngle = endAngle; + if (element.endAngle === element.startAngle) { + element.endAngle += TAU; + element.fullCircles--; } } ctx.beginPath(); - ctx.arc(model.x, model.y, model.innerRadius, model.startAngle + TAU, model.startAngle, true); - for (i = 0; i < model.fullCircles; ++i) { + ctx.arc(x, y, innerRadius, startAngle + TAU, startAngle, true); + for (i = 0; i < element.fullCircles; ++i) { ctx.stroke(); } ctx.beginPath(); - ctx.arc(model.x, model.y, element.outerRadius, model.startAngle, model.startAngle + TAU); - for (i = 0; i < model.fullCircles; ++i) { + ctx.arc(x, y, outerRadius, startAngle, startAngle + TAU); + for (i = 0; i < element.fullCircles; ++i) { ctx.stroke(); } } -function drawBorder(ctx, element, model) { - const options = element.options; +function drawBorder(ctx, element) { + const {x, y, startAngle, endAngle, pixelMargin, options} = element; + const outerRadius = element.outerRadius; + const innerRadius = element.innerRadius + pixelMargin; const inner = options.borderAlign === 'inner'; + if (!options.borderWidth) { + return; + } + if (inner) { ctx.lineWidth = options.borderWidth * 2; ctx.lineJoin = 'round'; @@ -84,17 +97,17 @@ function drawBorder(ctx, element, model) { ctx.lineJoin = 'bevel'; } - if (model.fullCircles) { - drawFullCircleBorders(ctx, element, model, inner); + if (element.fullCircles) { + drawFullCircleBorders(ctx, element, inner); } if (inner) { - clipArc(ctx, model); + clipArc(ctx, element); } ctx.beginPath(); - ctx.arc(model.x, model.y, element.outerRadius, model.startAngle, model.endAngle); - ctx.arc(model.x, model.y, model.innerRadius, model.endAngle, model.startAngle, true); + ctx.arc(x, y, outerRadius, startAngle, endAngle); + ctx.arc(x, y, innerRadius, endAngle, startAngle, true); ctx.closePath(); ctx.stroke(); } @@ -110,6 +123,8 @@ export default class Arc extends Element { this.endAngle = undefined; this.innerRadius = undefined; this.outerRadius = undefined; + this.pixelMargin = 0; + this.fullCircles = 0; if (cfg) { Object.assign(this, cfg); @@ -167,17 +182,9 @@ export default class Arc extends Element { draw(ctx) { const me = this; const options = me.options; - const pixelMargin = (options.borderAlign === 'inner') ? 0.33 : 0; - const model = { - x: me.x, - y: me.y, - innerRadius: me.innerRadius, - outerRadius: Math.max(me.outerRadius - pixelMargin, 0), - pixelMargin, - startAngle: me.startAngle, - endAngle: me.endAngle, - fullCircles: Math.floor(me.circumference / TAU) - }; + const offset = options.offset || 0; + me.pixelMargin = (options.borderAlign === 'inner') ? 0.33 : 0; + me.fullCircles = Math.floor(me.circumference / TAU); if (me.circumference === 0) { return; @@ -185,14 +192,16 @@ export default class Arc extends Element { ctx.save(); + if (offset && me.circumference < TAU) { + const halfAngle = (me.startAngle + me.endAngle) / 2; + ctx.translate(Math.cos(halfAngle) * offset, Math.sin(halfAngle) * offset); + } + ctx.fillStyle = options.backgroundColor; ctx.strokeStyle = options.borderColor; - drawArc(ctx, model, me.circumference); - - if (options.borderWidth) { - drawBorder(ctx, me, model); - } + drawArc(ctx, me); + drawBorder(ctx, me); ctx.restore(); } @@ -206,7 +215,8 @@ Arc.id = 'arc'; Arc.defaults = { borderAlign: 'center', borderColor: '#fff', - borderWidth: 2 + borderWidth: 2, + offset: 0 }; /**