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.
| 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.
## 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
* 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`
// 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();
}
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;
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;
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.
// 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;
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,
circumference: 360,
// The outr radius of the chart
- outerRadius: '100%'
+ radius: '100%'
},
indexAxis: 'r',
circumference: 360,
// The outr radius of the chart
- outerRadius: '100%'
+ radius: '100%'
}
};
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;
}]
},
options: {
- outerRadius: '30%',
+ radius: '30%',
}
}
};
}]
},
options: {
- outerRadius: 150,
+ radius: 150,
}
}
};
--- /dev/null
+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();
+ });
+ }
+ }
+};
animateRotate: true,
animateScale: false
},
- cutoutPercentage: 50,
+ cutout: '50%',
rotation: 0,
circumference: 360,
elements: {
legend: false,
title: false,
},
- cutoutPercentage: 50,
+ cutout: '50%',
rotation: 270,
circumference: 90,
elements: {
legend: false,
title: false
},
- cutoutPercentage: 50,
+ cutout: '50%',
rotation: 270,
circumference: 90,
elements: {
}]
},
options: {
- cutoutPercentage: 0,
+ cutout: '50%',
elements: {
arc: {
backgroundColor: 'rgb(100, 150, 200)',
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<number | string, ScriptableContext<number>>;
/**
* The outer radius of the chart. String ending with '%' means percentage of maximum radius, number means pixels.
* @default '100%'
*/
- outerRadius: Scriptable<number | string, ScriptableContext<number>>;
+ radius: Scriptable<number | string, ScriptableContext<number>>;
/**
* Starting angle to draw arcs from.
}]
},
options: {
- outerRadius: () => Math.random() > 0.5 ? 50 : '50%',
+ radius: () => Math.random() > 0.5 ? 50 : '50%',
}
});