'use strict'; var DatasetController = require('../core/core.datasetController'); var defaults = require('../core/core.defaults'); var elements = require('../elements/index'); var helpers = require('../helpers/index'); var valueOrDefault = helpers.valueOrDefault; var PI = Math.PI; var DOUBLE_PI = PI * 2; var HALF_PI = PI / 2; defaults._set('doughnut', { animation: { // Boolean - Whether we animate the rotation of the Doughnut animateRotate: true, // Boolean - Whether we animate scaling the Doughnut from the centre animateScale: false }, legendCallback: function(chart) { var list = document.createElement('ul'); var data = chart.data; var datasets = data.datasets; var labels = data.labels; var i, ilen, listItem, listItemSpan; list.setAttribute('class', chart.id + '-legend'); if (datasets.length) { for (i = 0, ilen = datasets[0].data.length; i < ilen; ++i) { listItem = list.appendChild(document.createElement('li')); listItemSpan = listItem.appendChild(document.createElement('span')); listItemSpan.style.backgroundColor = datasets[0].backgroundColor[i]; if (labels[i]) { listItem.appendChild(document.createTextNode(labels[i])); } } } return list.outerHTML; }, 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 (helpers.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; } } } }); module.exports = DatasetController.extend({ dataElementType: elements.Arc, linkScales: helpers.noop, /** * @private */ _dataElementOptions: [ 'backgroundColor', 'borderColor', 'borderWidth', 'borderAlign', 'hoverBackgroundColor', 'hoverBorderColor', 'hoverBorderWidth', ], /** * Override data parsing, since we are not using scales * @private */ _parse: function(start, count) { var data = this.getDataset().data; var metaData = this.getMeta().data; var i, ilen; for (i = start, ilen = start + count; i < ilen; ++i) { metaData[i]._val = +data[i]; } }, // Get index of the dataset in relation to the visible datasets. This allows determining the inner and outer radius correctly getRingIndex: function(datasetIndex) { var ringIndex = 0; for (var j = 0; j < datasetIndex; ++j) { if (this.chart.isDatasetVisible(j)) { ++ringIndex; } } return ringIndex; }, update: function(reset) { 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.getMeta(); 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); } 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); for (i = 0, ilen = arcs.length; i < ilen; ++i) { me.updateElement(arcs[i], i, reset); } }, updateElement: function(arc, index, reset) { var me = this; var chart = me.chart; var chartArea = chart.chartArea; var opts = chart.options; var animationOpts = opts.animation; var centerX = (chartArea.left + chartArea.right) / 2; var centerY = (chartArea.top + chartArea.bottom) / 2; var startAngle = opts.rotation; // non reset case handled later var endAngle = opts.rotation; // non reset case handled later var circumference = reset && animationOpts.animateRotate ? 0 : arc.hidden ? 0 : me.calculateCircumference(arc._val * opts.circumference / DOUBLE_PI); var innerRadius = reset && animationOpts.animateScale ? 0 : me.innerRadius; var outerRadius = reset && animationOpts.animateScale ? 0 : me.outerRadius; var options = arc._options || {}; helpers.extend(arc, { // Desired view properties _model: { backgroundColor: options.backgroundColor, borderColor: options.borderColor, borderWidth: options.borderWidth, borderAlign: options.borderAlign, x: centerX + chart.offsetX, y: centerY + chart.offsetY, startAngle: startAngle, endAngle: endAngle, circumference: circumference, outerRadius: outerRadius, innerRadius: innerRadius } }); var model = arc._model; // Set correct angles if not resetting if (!reset || !animationOpts.animateRotate) { if (index === 0) { model.startAngle = opts.rotation; } else { model.startAngle = me.getMeta().data[index - 1]._model.endAngle; } model.endAngle = model.startAngle + model.circumference; } arc.pivot(chart._animationsDisabled); }, calculateTotal: function() { var metaData = this.getMeta().data; var total = 0; var value; helpers.each(metaData, function(arc) { value = arc ? arc._val : NaN; if (!isNaN(value) && !arc.hidden) { total += Math.abs(value); } }); /* if (total === 0) { total = NaN; }*/ return total; }, calculateCircumference: function(value) { var total = this.getMeta().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: function(arcs) { var me = this; var max = 0; var chart = me.chart; var i, ilen, meta, arc, controller, options, borderWidth, hoverWidth; 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; if (i !== me.index) { controller = meta.controller; } break; } } } if (!arcs) { return 0; } for (i = 0, ilen = arcs.length; i < ilen; ++i) { arc = arcs[i]; if (controller) { controller._configure(); options = controller._resolveDataElementOptions(i); } else { options = arc._options; } if (options.borderAlign !== 'inner') { borderWidth = options.borderWidth; hoverWidth = options.hoverBorderWidth; max = borderWidth > max ? borderWidth : max; max = hoverWidth > max ? hoverWidth : max; } } return max; }, /** * @protected */ setHoverStyle: function(arc) { var model = arc._model; var options = arc._options; var getHoverColor = helpers.getHoverColor; arc.$previousStyle = { backgroundColor: model.backgroundColor, borderColor: model.borderColor, borderWidth: model.borderWidth, }; model.backgroundColor = valueOrDefault(options.hoverBackgroundColor, getHoverColor(options.backgroundColor)); model.borderColor = valueOrDefault(options.hoverBorderColor, getHoverColor(options.borderColor)); model.borderWidth = valueOrDefault(options.hoverBorderWidth, options.borderWidth); }, /** * 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: function(datasetIndex) { var ringWeightOffset = 0; for (var i = 0; i < datasetIndex; ++i) { if (this.chart.isDatasetVisible(i)) { ringWeightOffset += this._getRingWeight(i); } } return ringWeightOffset; }, /** * @private */ _getRingWeight: function(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: function() { return this._getRingWeightOffset(this.chart.data.datasets.length); } });