diff --git a/docs/docs/charts/doughnut.mdx b/docs/docs/charts/doughnut.mdx index 63abd025f90..d34e296b010 100644 --- a/docs/docs/charts/doughnut.mdx +++ b/docs/docs/charts/doughnut.mdx @@ -6,7 +6,7 @@ Pie and doughnut charts are probably the most commonly used charts. They are div They are excellent at showing the relational proportions between data. -Pie and doughnut charts are effectively the same class in Chart.js, but have one different default value - their `cutoutPercentage`. This equates to what percentage of the inner should be cut out. This defaults to `0` for pie charts, and `50` for doughnuts. +Pie and doughnut charts are effectively the same class in Chart.js, but have one different default value - their `cutout`. This equates to what portion of the inner should be cut out. This defaults to `0` for pie charts, and `'50%'` for doughnuts. 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. @@ -163,8 +163,8 @@ These are the customisation options specific to Pie & Doughnut charts. These opt | Name | Type | Default | Description | ---- | ---- | ------- | ----------- -| `cutoutPercentage` | `number` | `50` - for doughnut, `0` - for pie | The percentage of the chart that is cut out of the middle. -| `outerRadius` | `number`\|`string` | `100%` | The outer radius of the chart. If `string` and ending with '%', percentage of the maximum radius. `number` is considered to be pixels. +| `cutout` | `number`\|`string` | `50%` - for doughnut, `0` - for pie | The portion of the chart that is cut out of the middle. If `string` and ending with '%', percentage of the chart radius. `number` is considered to be pixels. +| `radius` | `number`\|`string` | `100%` | The outer radius of the chart. If `string` and ending with '%', percentage of the maximum radius. `number` is considered to be pixels. | `rotation` | `number` | 0 | Starting angle to draw arcs from. | `circumference` | `number` | 360 | Sweep to allow arcs to cover. | `animation.animateRotate` | `boolean` | `true` | If true, the chart will animate in with a rotation animation. This property is in the `options.animation` object. @@ -172,7 +172,7 @@ These are the customisation options specific to Pie & Doughnut charts. These opt ## Default Options -We can also change these default values for each Doughnut type that is created, this object is available at `Chart.defaults.controllers.doughnut`. Pie charts also have a clone of these defaults available to change at `Chart.defaults.controllers.pie`, with the only difference being `cutoutPercentage` being set to 0. +We can also change these default values for each Doughnut type that is created, this object is available at `Chart.defaults.controllers.doughnut`. Pie charts also have a clone of these defaults available to change at `Chart.defaults.controllers.pie`, with the only difference being `cutout` being set to 0. ## Data Structure diff --git a/docs/docs/getting-started/v3-migration.md b/docs/docs/getting-started/v3-migration.md index 332779e9a34..6b9137911cf 100644 --- a/docs/docs/getting-started/v3-migration.md +++ b/docs/docs/getting-started/v3-migration.md @@ -77,6 +77,7 @@ A number of changes were made to the configuration options passed to the `Chart` * Polar area `startAngle` option is now consistent with `Radar`, 0 is at top and value is in degrees. Default is changed from `-½π` to `0`. * Doughnut `rotation` option is now in degrees and 0 is at top. Default is changed from `-½π` to `0`. * Doughnut `circumference` option is now in degrees. Default is changed from `2π` to `360`. +* Doughnut `cutoutPercentage` was renamed to `cutout`and accepts pixels as numer and percent as string ending with `%`. * `scale` option was removed in favor of `options.scales.r` (or any other scale id, with `axis: 'r'`) * `scales.[x/y]Axes` arrays were removed. Scales are now configured directly to `options.scales` object with the object key being the scale Id. * `scales.[x/y]Axes.barPercentage` was moved to dataset option `barPercentage` diff --git a/samples/scriptable/pie.html b/samples/scriptable/pie.html index 2b5b26c619b..1cc10366e39 100644 --- a/samples/scriptable/pie.html +++ b/samples/scriptable/pie.html @@ -99,10 +99,10 @@ // eslint-disable-next-line no-unused-vars function togglePieDoughnut() { - if (chart.options.cutoutPercentage) { - chart.options.cutoutPercentage = 0; + if (chart.options.cutout) { + chart.options.cutout = 0; } else { - chart.options.cutoutPercentage = 50; + chart.options.cutout = '50%'; } chart.update(); } diff --git a/src/controllers/controller.doughnut.js b/src/controllers/controller.doughnut.js index 2f013c0cff8..d2b75f32a97 100644 --- a/src/controllers/controller.doughnut.js +++ b/src/controllers/controller.doughnut.js @@ -1,13 +1,12 @@ import DatasetController from '../core/core.datasetController'; import {formatNumber} from '../core/core.intl'; -import {isArray, numberOrPercentageOf, valueOrDefault} from '../helpers/helpers.core'; -import {toRadians, PI, TAU, HALF_PI} from '../helpers/helpers.math'; +import {isArray, toPercentage, toPixels, valueOrDefault} from '../helpers/helpers.core'; +import {toRadians, PI, TAU, HALF_PI, _angleBetween} from '../helpers/helpers.math'; /** * @typedef { import("../core/core.controller").default } Chart */ - function getRatioAndOffset(rotation, circumference, cutout) { let ratioX = 1; let ratioY = 1; @@ -15,21 +14,18 @@ function getRatioAndOffset(rotation, circumference, cutout) { let offsetY = 0; // If the chart's circumference isn't a full circle, calculate size as a ratio of the width/height of the arc if (circumference < TAU) { - let startAngle = rotation % TAU; - startAngle += startAngle >= PI ? -TAU : startAngle < -PI ? TAU : 0; + const startAngle = rotation; const endAngle = startAngle + circumference; const startX = Math.cos(startAngle); const startY = Math.sin(startAngle); const endX = Math.cos(endAngle); const endY = Math.sin(endAngle); - const contains0 = (startAngle <= 0 && endAngle >= 0) || endAngle >= TAU; - const contains90 = (startAngle <= HALF_PI && endAngle >= HALF_PI) || endAngle >= TAU + HALF_PI; - const contains180 = startAngle === -PI || endAngle >= PI; - const contains270 = (startAngle <= -HALF_PI && endAngle >= -HALF_PI) || endAngle >= PI + HALF_PI; - const minX = contains180 ? -1 : Math.min(startX, startX * cutout, endX, endX * cutout); - const minY = contains270 ? -1 : Math.min(startY, startY * cutout, endY, endY * cutout); - const maxX = contains0 ? 1 : Math.max(startX, startX * cutout, endX, endX * cutout); - const maxY = contains90 ? 1 : Math.max(startY, startY * cutout, endY, endY * cutout); + const calcMax = (angle, a, b) => _angleBetween(angle, startAngle, endAngle) ? 1 : Math.max(a, a * cutout, b, b * cutout); + const calcMin = (angle, a, b) => _angleBetween(angle, startAngle, endAngle) ? -1 : Math.min(a, a * cutout, b, b * cutout); + const maxX = calcMax(0, startX, endX); + const maxY = calcMax(HALF_PI, startY, endY); + const minX = calcMin(PI, startX, endX); + const minY = calcMin(PI + HALF_PI, startY, endY); ratioX = (maxX - minX) / 2; ratioY = (maxY - minY) / 2; offsetX = -(maxX + minX) / 2; @@ -127,7 +123,9 @@ export default class DoughnutController extends DatasetController { const {chartArea} = chart; const meta = me._cachedMeta; const arcs = meta.data; - const cutout = me.options.cutoutPercentage / 100 || 0; + const spacing = me.getMaxBorderWidth() + me.getMaxOffset(arcs); + const maxSize = Math.max((Math.min(chartArea.width, chartArea.height) - spacing) / 2, 0); + const cutout = Math.min(toPercentage(me.options.cutout, maxSize), 1); const chartWeight = me._getRingWeight(me.index); // Compute the maximal rotation & circumference limits. @@ -135,11 +133,10 @@ export default class DoughnutController extends DatasetController { // are both less than a circle with different rotations (starting angles) const {circumference, rotation} = me._getRotationExtents(); const {ratioX, ratioY, offsetX, offsetY} = getRatioAndOffset(rotation, circumference, cutout); - const spacing = me.getMaxBorderWidth() + me.getMaxOffset(arcs); - const maxWidth = (chartArea.right - chartArea.left - spacing) / ratioX; - const maxHeight = (chartArea.bottom - chartArea.top - spacing) / ratioY; + const maxWidth = (chartArea.width - spacing) / ratioX; + const maxHeight = (chartArea.height - spacing) / ratioY; const maxRadius = Math.max(Math.min(maxWidth, maxHeight) / 2, 0); - const outerRadius = numberOrPercentageOf(me.options.outerRadius, maxRadius); + const outerRadius = toPixels(me.options.radius, maxRadius); const innerRadius = Math.max(outerRadius * cutout, 0); const radiusLength = (outerRadius - innerRadius) / me._getVisibleDatasetWeightTotal(); me.offsetX = offsetX * outerRadius; @@ -345,7 +342,7 @@ DoughnutController.defaults = { datasets: { // The percentage of the chart that we cut out of the middle. - cutoutPercentage: 50, + cutout: '50%', // The rotation of the chart, where the first data arc begins. rotation: 0, @@ -354,7 +351,7 @@ DoughnutController.defaults = { circumference: 360, // The outr radius of the chart - outerRadius: '100%' + radius: '100%' }, indexAxis: 'r', diff --git a/src/controllers/controller.pie.js b/src/controllers/controller.pie.js index f0162ef2d04..a53b8d5f9ed 100644 --- a/src/controllers/controller.pie.js +++ b/src/controllers/controller.pie.js @@ -22,6 +22,6 @@ PieController.defaults = { circumference: 360, // The outr radius of the chart - outerRadius: '100%' + radius: '100%' } }; diff --git a/src/helpers/helpers.core.js b/src/helpers/helpers.core.js index f670065c36f..78ce4740970 100644 --- a/src/helpers/helpers.core.js +++ b/src/helpers/helpers.core.js @@ -85,7 +85,12 @@ export function valueOrDefault(value, defaultValue) { return typeof value === 'undefined' ? defaultValue : value; } -export const numberOrPercentageOf = (value, dimension) => +export const toPercentage = (value, dimension) => + typeof value === 'string' && value.endsWith('%') ? + parseFloat(value) / 100 + : value / dimension; + +export const toPixels = (value, dimension) => typeof value === 'string' && value.endsWith('%') ? parseFloat(value) / 100 * dimension : +value; diff --git a/test/fixtures/controller.doughnut/doughnut-outer-radius-percent.js b/test/fixtures/controller.doughnut/doughnut-outer-radius-percent.js index 28dd46da09b..46cf4b7200b 100644 --- a/test/fixtures/controller.doughnut/doughnut-outer-radius-percent.js +++ b/test/fixtures/controller.doughnut/doughnut-outer-radius-percent.js @@ -22,7 +22,7 @@ module.exports = { }] }, options: { - outerRadius: '30%', + radius: '30%', } } }; diff --git a/test/fixtures/controller.doughnut/doughnut-outer-radius-pixels.js b/test/fixtures/controller.doughnut/doughnut-outer-radius-pixels.js index a4a50df2387..8fabaa935ac 100644 --- a/test/fixtures/controller.doughnut/doughnut-outer-radius-pixels.js +++ b/test/fixtures/controller.doughnut/doughnut-outer-radius-pixels.js @@ -22,7 +22,7 @@ module.exports = { }] }, options: { - outerRadius: 150, + radius: 150, } } }; diff --git a/test/fixtures/controller.doughnut/doughnut-rotation-circumference-8x8.js b/test/fixtures/controller.doughnut/doughnut-rotation-circumference-8x8.js new file mode 100644 index 00000000000..4a0481ef8f2 --- /dev/null +++ b/test/fixtures/controller.doughnut/doughnut-rotation-circumference-8x8.js @@ -0,0 +1,67 @@ +const canvas = document.createElement('canvas'); +canvas.width = 512; +canvas.height = 512; +const ctx = canvas.getContext('2d'); + +module.exports = { + config: { + type: 'doughnut', + data: { + labels: ['A', 'B', 'C', 'D', 'E'], + datasets: [{ + data: [1, 5, 10, 50, 100], + backgroundColor: [ + 'rgba(255, 99, 132, 0.8)', + 'rgba(54, 162, 235, 0.8)', + 'rgba(255, 206, 86, 0.8)', + 'rgba(75, 192, 192, 0.8)', + 'rgba(153, 102, 255, 0.8)' + ], + borderColor: [ + 'rgb(255, 99, 132)', + 'rgb(54, 162, 235)', + 'rgb(255, 206, 86)', + 'rgb(75, 192, 192)', + 'rgb(153, 102, 255)' + ] + }] + }, + options: { + rotation: -360, + circumference: 180, + events: [] + } + }, + options: { + canvas: { + height: 512, + width: 512 + }, + run: function(chart) { + return new Promise((resolve) => { + for (let i = 0; i < 64; i++) { + const col = i % 8; + const row = Math.floor(i / 8); + const evenodd = row % 2 ? 1 : -1; + chart.options.rotation = col * 45 * evenodd; + chart.options.circumference = 360 - row * 45; + chart.update(); + ctx.drawImage(chart.canvas, col * 64, row * 64, 64, 64); + } + ctx.strokeStyle = 'red'; + ctx.lineWidth = 0.5; + ctx.beginPath(); + for (let i = 1; i < 8; i++) { + ctx.moveTo(i * 64, 0); + ctx.lineTo(i * 64, 511); + ctx.moveTo(0, i * 64); + ctx.lineTo(511, i * 64); + } + ctx.stroke(); + Chart.helpers.clearCanvas(chart.canvas); + chart.ctx.drawImage(canvas, 0, 0); + resolve(); + }); + } + } +}; diff --git a/test/fixtures/controller.doughnut/doughnut-rotation-circumference-8x8.png b/test/fixtures/controller.doughnut/doughnut-rotation-circumference-8x8.png new file mode 100644 index 00000000000..ec18f135afa Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-rotation-circumference-8x8.png differ diff --git a/test/specs/controller.doughnut.tests.js b/test/specs/controller.doughnut.tests.js index 9be7c86f20a..c3bb1d9f2be 100644 --- a/test/specs/controller.doughnut.tests.js +++ b/test/specs/controller.doughnut.tests.js @@ -70,7 +70,7 @@ describe('Chart.controllers.doughnut', function() { animateRotate: true, animateScale: false }, - cutoutPercentage: 50, + cutout: '50%', rotation: 0, circumference: 360, elements: { @@ -169,7 +169,7 @@ describe('Chart.controllers.doughnut', function() { legend: false, title: false, }, - cutoutPercentage: 50, + cutout: '50%', rotation: 270, circumference: 90, elements: { @@ -215,7 +215,7 @@ describe('Chart.controllers.doughnut', function() { legend: false, title: false }, - cutoutPercentage: 50, + cutout: '50%', rotation: 270, circumference: 90, elements: { @@ -325,7 +325,7 @@ describe('Chart.controllers.doughnut', function() { }] }, options: { - cutoutPercentage: 0, + cutout: '50%', elements: { arc: { backgroundColor: 'rgb(100, 150, 200)', diff --git a/types/index.esm.d.ts b/types/index.esm.d.ts index 7a3d28df561..5231d1f34dd 100644 --- a/types/index.esm.d.ts +++ b/types/index.esm.d.ts @@ -285,16 +285,17 @@ export interface DoughnutControllerChartOptions { circumference: number; /** - * The percentage of the chart that is cut out of the middle. (50 - for doughnut, 0 - for pie) + * The portion of the chart that is cut out of the middle. ('50%' - for doughnut, 0 - for pie) + * String ending with '%' means percentage, number means pixels. * @default 50 */ - cutoutPercentage: number; + cutout: Scriptable>; /** * The outer radius of the chart. String ending with '%' means percentage of maximum radius, number means pixels. * @default '100%' */ - outerRadius: Scriptable>; + radius: Scriptable>; /** * Starting angle to draw arcs from. diff --git a/types/tests/controllers/doughnut_outer_radius.ts b/types/tests/controllers/doughnut_outer_radius.ts index 2c1dd73b751..e1074f39cad 100644 --- a/types/tests/controllers/doughnut_outer_radius.ts +++ b/types/tests/controllers/doughnut_outer_radius.ts @@ -9,6 +9,6 @@ const chart = new Chart('id', { }] }, options: { - outerRadius: () => Math.random() > 0.5 ? 50 : '50%', + radius: () => Math.random() > 0.5 ? 50 : '50%', } });