mirror of
https://github.com/chartjs/Chart.js.git
synced 2025-12-08 20:36:08 +00:00
357 lines
9.5 KiB
JavaScript
357 lines
9.5 KiB
JavaScript
'use strict';
|
|
|
|
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';
|
|
|
|
const PI = Math.PI;
|
|
const DOUBLE_PI = PI * 2;
|
|
const HALF_PI = PI / 2;
|
|
|
|
defaults.set('doughnut', {
|
|
animation: {
|
|
numbers: {
|
|
type: 'number',
|
|
properties: ['x', 'y', 'startAngle', 'endAngle', 'innerRadius', 'outerRadius']
|
|
},
|
|
// 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: function(chart) {
|
|
var data = chart.data;
|
|
if (data.labels.length && data.datasets.length) {
|
|
return data.labels.map(function(label, i) {
|
|
var meta = chart.getDatasetMeta(0);
|
|
var 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: function(e, legendItem) {
|
|
var index = legendItem.index;
|
|
var chart = this.chart;
|
|
var 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: function() {
|
|
return '';
|
|
},
|
|
label: function(tooltipItem, data) {
|
|
var dataLabel = data.labels[tooltipItem.index];
|
|
var 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;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
class DoughnutController extends DatasetController {
|
|
|
|
constructor(chart, datasetIndex) {
|
|
super(chart, datasetIndex);
|
|
|
|
this.innerRadius = undefined;
|
|
this.outerRadius = undefined;
|
|
}
|
|
|
|
linkScales() {}
|
|
|
|
/**
|
|
* Override data parsing, since we are not using scales
|
|
* @private
|
|
*/
|
|
_parse(start, count) {
|
|
var data = this.getDataset().data;
|
|
var meta = this._cachedMeta;
|
|
var 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) {
|
|
var ringIndex = 0;
|
|
|
|
for (var j = 0; j < datasetIndex; ++j) {
|
|
if (this.chart.isDatasetVisible(j)) {
|
|
++ringIndex;
|
|
}
|
|
}
|
|
|
|
return ringIndex;
|
|
}
|
|
|
|
update(mode) {
|
|
var me = this;
|
|
var chart = me.chart;
|
|
var chartArea = chart.chartArea;
|
|
var opts = chart.options;
|
|
var ratioX = 1;
|
|
var ratioY = 1;
|
|
var offsetX = 0;
|
|
var offsetY = 0;
|
|
var meta = me._cachedMeta;
|
|
var arcs = meta.data;
|
|
var cutout = opts.cutoutPercentage / 100 || 0;
|
|
var circumference = opts.circumference;
|
|
var chartWeight = me._getRingWeight(me.index);
|
|
var maxWidth, maxHeight, i, ilen;
|
|
|
|
// 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) {
|
|
var startAngle = opts.rotation % DOUBLE_PI;
|
|
startAngle += startAngle >= PI ? -DOUBLE_PI : startAngle < -PI ? DOUBLE_PI : 0;
|
|
var endAngle = startAngle + circumference;
|
|
var startX = Math.cos(startAngle);
|
|
var startY = Math.sin(startAngle);
|
|
var endX = Math.cos(endAngle);
|
|
var endY = Math.sin(endAngle);
|
|
var contains0 = (startAngle <= 0 && endAngle >= 0) || endAngle >= DOUBLE_PI;
|
|
var contains90 = (startAngle <= HALF_PI && endAngle >= HALF_PI) || endAngle >= DOUBLE_PI + HALF_PI;
|
|
var contains180 = startAngle === -PI || endAngle >= PI;
|
|
var contains270 = (startAngle <= -HALF_PI && endAngle >= -HALF_PI) || endAngle >= PI + HALF_PI;
|
|
var minX = contains180 ? -1 : Math.min(startX, startX * cutout, endX, endX * cutout);
|
|
var minY = contains270 ? -1 : Math.min(startY, startY * cutout, endY, endY * cutout);
|
|
var maxX = contains0 ? 1 : Math.max(startX, startX * cutout, endX, endX * cutout);
|
|
var 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;
|
|
}
|
|
|
|
for (i = 0, ilen = arcs.length; i < ilen; ++i) {
|
|
arcs[i]._options = me._resolveDataElementOptions(i, mode);
|
|
}
|
|
|
|
chart.borderWidth = me.getMaxBorderWidth();
|
|
maxWidth = (chartArea.right - chartArea.left - chart.borderWidth) / ratioX;
|
|
maxHeight = (chartArea.bottom - chartArea.top - chart.borderWidth) / ratioY;
|
|
chart.outerRadius = Math.max(Math.min(maxWidth, maxHeight) / 2, 0);
|
|
chart.innerRadius = Math.max(chart.outerRadius * cutout, 0);
|
|
chart.radiusLength = (chart.outerRadius - chart.innerRadius) / (me._getVisibleDatasetWeightTotal() || 1);
|
|
chart.offsetX = offsetX * chart.outerRadius;
|
|
chart.offsetY = offsetY * chart.outerRadius;
|
|
|
|
meta.total = me.calculateTotal();
|
|
|
|
me.outerRadius = chart.outerRadius - chart.radiusLength * me._getRingWeightOffset(me.index);
|
|
me.innerRadius = Math.max(me.outerRadius - chart.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 innerRadius = reset && animationOpts.animateScale ? 0 : me.innerRadius;
|
|
const outerRadius = reset && animationOpts.animateScale ? 0 : me.outerRadius;
|
|
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 options = arc._options || {};
|
|
const properties = {
|
|
x: centerX + chart.offsetX,
|
|
y: centerY + chart.offsetY,
|
|
startAngle,
|
|
endAngle: startAngle + circumference,
|
|
circumference,
|
|
outerRadius,
|
|
innerRadius,
|
|
options
|
|
};
|
|
startAngle += circumference;
|
|
|
|
me._updateElement(arc, index, properties, 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);
|
|
}
|
|
}
|
|
|
|
/* if (total === 0) {
|
|
total = NaN;
|
|
}*/
|
|
|
|
return total;
|
|
}
|
|
|
|
calculateCircumference(value) {
|
|
var total = this._cachedMeta.total;
|
|
if (total > 0 && !isNaN(value)) {
|
|
return DOUBLE_PI * (Math.abs(value) / total);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// gets the max border or hover width to properly scale pie charts
|
|
getMaxBorderWidth(arcs) {
|
|
var me = this;
|
|
var max = 0;
|
|
var chart = me.chart;
|
|
var 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) {
|
|
var ringWeightOffset = 0;
|
|
|
|
for (var 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. This value can be 0.
|
|
* @private
|
|
*/
|
|
_getVisibleDatasetWeightTotal() {
|
|
return this._getRingWeightOffset(this.chart.data.datasets.length);
|
|
}
|
|
}
|
|
|
|
DoughnutController.prototype.dataElementType = Arc;
|
|
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
DoughnutController.prototype._dataElementOptions = [
|
|
'backgroundColor',
|
|
'borderColor',
|
|
'borderWidth',
|
|
'borderAlign',
|
|
'hoverBackgroundColor',
|
|
'hoverBorderColor',
|
|
'hoverBorderWidth',
|
|
];
|
|
|
|
export default DoughnutController;
|