Chart.js/src/controllers/controller.doughnut.js
2020-02-20 12:18:21 -05:00

359 lines
9.8 KiB
JavaScript

import DatasetController from '../core/core.datasetController';
import defaults from '../core/core.defaults';
import Arc from '../elements/element.arc';
import {isArray, valueOrDefault} from '../helpers/helpers.core';
/**
* @typedef { import("../core/core.controller").default } Chart
*/
const PI = Math.PI;
const DOUBLE_PI = PI * 2;
const HALF_PI = PI / 2;
defaults.set('doughnut', {
animation: {
numbers: {
type: 'number',
properties: ['circumference', 'endAngle', 'innerRadius', 'outerRadius', 'startAngle', 'x', 'y']
},
// Boolean - Whether we animate the rotation of the Doughnut
animateRotate: true,
// Boolean - Whether we animate scaling the Doughnut from the centre
animateScale: false
},
legend: {
labels: {
generateLabels(chart) {
const data = chart.data;
if (data.labels.length && data.datasets.length) {
return data.labels.map((label, i) => {
const meta = chart.getDatasetMeta(0);
const style = meta.controller.getStyle(i);
return {
text: label,
fillStyle: style.backgroundColor,
strokeStyle: style.borderColor,
lineWidth: style.borderWidth,
hidden: isNaN(data.datasets[0].data[i]) || meta.data[i].hidden,
// Extra data used for toggling the correct item
index: i
};
});
}
return [];
}
},
onClick(e, legendItem) {
const index = legendItem.index;
const chart = this.chart;
let i, ilen, meta;
for (i = 0, ilen = (chart.data.datasets || []).length; i < ilen; ++i) {
meta = chart.getDatasetMeta(i);
// toggle visibility of index if exists
if (meta.data[index]) {
meta.data[index].hidden = !meta.data[index].hidden;
}
}
chart.update();
}
},
// The percentage of the chart that we cut out of the middle.
cutoutPercentage: 50,
// The rotation of the chart, where the first data arc begins.
rotation: -HALF_PI,
// The total circumference of the chart.
circumference: DOUBLE_PI,
// Need to override these to give a nice default
tooltips: {
callbacks: {
title() {
return '';
},
label(tooltipItem, data) {
let dataLabel = data.labels[tooltipItem.index];
const value = ': ' + data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];
if (isArray(dataLabel)) {
// show value on first line of multiline label
// need to clone because we are changing the value
dataLabel = dataLabel.slice();
dataLabel[0] += value;
} else {
dataLabel += value;
}
return dataLabel;
}
}
}
});
function getRatioAndOffset(rotation, circumference, cutout) {
let ratioX = 1;
let ratioY = 1;
let offsetX = 0;
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 < DOUBLE_PI) {
let startAngle = rotation % DOUBLE_PI;
startAngle += startAngle >= PI ? -DOUBLE_PI : startAngle < -PI ? DOUBLE_PI : 0;
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 >= DOUBLE_PI;
const contains90 = (startAngle <= HALF_PI && endAngle >= HALF_PI) || endAngle >= DOUBLE_PI + 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);
ratioX = (maxX - minX) / 2;
ratioY = (maxY - minY) / 2;
offsetX = -(maxX + minX) / 2;
offsetY = -(maxY + minY) / 2;
}
return {ratioX, ratioY, offsetX, offsetY};
}
export default class DoughnutController extends DatasetController {
constructor(chart, datasetIndex) {
super(chart, datasetIndex);
this.innerRadius = undefined;
this.outerRadius = undefined;
this.offsetX = undefined;
this.offsetY = undefined;
}
linkScales() {}
/**
* Override data parsing, since we are not using scales
* @private
*/
_parse(start, count) {
const data = this.getDataset().data;
const meta = this._cachedMeta;
let i, ilen;
for (i = start, ilen = start + count; i < ilen; ++i) {
meta._parsed[i] = +data[i];
}
}
// Get index of the dataset in relation to the visible datasets. This allows determining the inner and outer radius correctly
getRingIndex(datasetIndex) {
let ringIndex = 0;
for (let j = 0; j < datasetIndex; ++j) {
if (this.chart.isDatasetVisible(j)) {
++ringIndex;
}
}
return ringIndex;
}
/**
* @param {string} mode
*/
update(mode) {
const me = this;
const chart = me.chart;
const {chartArea, options} = chart;
const meta = me._cachedMeta;
const arcs = meta.data;
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 outerRadius = Math.max(Math.min(maxWidth, maxHeight) / 2, 0);
const innerRadius = Math.max(outerRadius * cutout, 0);
const radiusLength = (outerRadius - innerRadius) / me._getVisibleDatasetWeightTotal();
me.offsetX = offsetX * outerRadius;
me.offsetY = offsetY * outerRadius;
meta.total = me.calculateTotal();
me.outerRadius = outerRadius - radiusLength * me._getRingWeightOffset(me.index);
me.innerRadius = Math.max(me.outerRadius - radiusLength * chartWeight, 0);
me.updateElements(arcs, 0, mode);
}
/**
* @private
*/
_circumference(i, reset) {
const me = this;
const opts = me.chart.options;
const meta = me._cachedMeta;
return reset && opts.animation.animateRotate ? 0 : meta.data[i].hidden ? 0 : me.calculateCircumference(meta._parsed[i] * opts.circumference / DOUBLE_PI);
}
updateElements(arcs, start, mode) {
const me = this;
const reset = mode === 'reset';
const chart = me.chart;
const chartArea = chart.chartArea;
const opts = chart.options;
const animationOpts = opts.animation;
const centerX = (chartArea.left + chartArea.right) / 2;
const centerY = (chartArea.top + chartArea.bottom) / 2;
const animateScale = reset && animationOpts.animateScale;
const innerRadius = animateScale ? 0 : me.innerRadius;
const outerRadius = animateScale ? 0 : me.outerRadius;
const firstOpts = me._resolveDataElementOptions(start, mode);
const sharedOptions = me._getSharedOptions(mode, arcs[start], firstOpts);
const includeOptions = me._includeOptions(mode, sharedOptions);
let startAngle = opts.rotation;
let i;
for (i = 0; i < start; ++i) {
startAngle += me._circumference(i, reset);
}
for (i = 0; i < arcs.length; ++i) {
const index = start + i;
const circumference = me._circumference(index, reset);
const arc = arcs[i];
const properties = {
x: centerX + me.offsetX,
y: centerY + me.offsetY,
startAngle,
endAngle: startAngle + circumference,
circumference,
outerRadius,
innerRadius
};
if (includeOptions) {
properties.options = me._resolveDataElementOptions(index, mode);
}
startAngle += circumference;
me._updateElement(arc, index, properties, mode);
}
me._updateSharedOptions(sharedOptions, mode);
}
calculateTotal() {
const meta = this._cachedMeta;
const metaData = meta.data;
let total = 0;
let i;
for (i = 0; i < metaData.length; i++) {
const value = meta._parsed[i];
if (!isNaN(value) && !metaData[i].hidden) {
total += Math.abs(value);
}
}
return total;
}
calculateCircumference(value) {
const total = this._cachedMeta.total;
if (total > 0 && !isNaN(value)) {
return DOUBLE_PI * (Math.abs(value) / total);
}
return 0;
}
getMaxBorderWidth(arcs) {
const me = this;
let max = 0;
const chart = me.chart;
let i, ilen, meta, controller, options;
if (!arcs) {
// Find the outmost visible dataset
for (i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) {
if (chart.isDatasetVisible(i)) {
meta = chart.getDatasetMeta(i);
arcs = meta.data;
controller = meta.controller;
if (controller !== me) {
controller._configure();
}
break;
}
}
}
if (!arcs) {
return 0;
}
for (i = 0, ilen = arcs.length; i < ilen; ++i) {
options = controller._resolveDataElementOptions(i);
if (options.borderAlign !== 'inner') {
max = Math.max(max, options.borderWidth || 0, options.hoverBorderWidth || 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
*/
_getRingWeightOffset(datasetIndex) {
let ringWeightOffset = 0;
for (let i = 0; i < datasetIndex; ++i) {
if (this.chart.isDatasetVisible(i)) {
ringWeightOffset += this._getRingWeight(i);
}
}
return ringWeightOffset;
}
/**
* @private
*/
_getRingWeight(datasetIndex) {
return Math.max(valueOrDefault(this.chart.data.datasets[datasetIndex].weight, 1), 0);
}
/**
* Returns the sum of all visibile data set weights.
* @private
*/
_getVisibleDatasetWeightTotal() {
return this._getRingWeightOffset(this.chart.data.datasets.length) || 1;
}
}
DoughnutController.prototype.dataElementType = Arc;
/**
* @private
*/
DoughnutController.prototype._dataElementOptions = [
'backgroundColor',
'borderColor',
'borderWidth',
'borderAlign',
'hoverBackgroundColor',
'hoverBorderColor',
'hoverBorderWidth',
];