"use strict"; module.exports = function(Chart) { var helpers = Chart.helpers; //Create a dictionary of chart types, to allow for extension of existing types Chart.types = {}; //Store a reference to each instance - allowing us to globally resize chart instances on window resize. //Destroy method on the chart will remove the instance of the chart from this reference. Chart.instances = {}; // Controllers available for dataset visualization eg. bar, line, slice, etc. Chart.controllers = {}; /** * @class Chart.Controller * The main controller of a chart. */ Chart.Controller = function(instance) { this.chart = instance; this.config = instance.config; this.options = this.config.options = helpers.configMerge(Chart.defaults.global, Chart.defaults[this.config.type], this.config.options || {}); this.id = helpers.uid(); Object.defineProperty(this, 'data', { get: function() { return this.config.data; } }); //Add the chart instance to the global namespace Chart.instances[this.id] = this; if (this.options.responsive) { // Silent resize before chart draws this.resize(true); } this.initialize(); return this; }; helpers.extend(Chart.Controller.prototype, /** @lends Chart.Controller */ { initialize: function() { var me = this; // Before init plugin notification Chart.plugins.notify('beforeInit', [me]); me.bindEvents(); // Make sure controllers are built first so that each dataset is bound to an axis before the scales // are built me.ensureScalesHaveIDs(); me.buildOrUpdateControllers(); me.buildScales(); me.updateLayout(); me.resetElements(); me.initToolTip(); me.update(); // After init plugin notification Chart.plugins.notify('afterInit', [me]); return me; }, clear: function() { helpers.clear(this.chart); return this; }, stop: function() { // Stops any current animation loop occuring Chart.animationService.cancelAnimation(this); return this; }, resize: function resize(silent) { var me = this; var chart = me.chart; var canvas = chart.canvas; var newWidth = helpers.getMaximumWidth(canvas); var aspectRatio = chart.aspectRatio; var newHeight = (me.options.maintainAspectRatio && isNaN(aspectRatio) === false && isFinite(aspectRatio) && aspectRatio !== 0) ? newWidth / aspectRatio : helpers.getMaximumHeight(canvas); var sizeChanged = chart.width !== newWidth || chart.height !== newHeight; if (!sizeChanged) { return me; } canvas.width = chart.width = newWidth; canvas.height = chart.height = newHeight; helpers.retinaScale(chart); // Notify any plugins about the resize var newSize = { width: newWidth, height: newHeight }; Chart.plugins.notify('resize', [me, newSize]); // Notify of resize if (me.options.onResize) { me.options.onResize(me, newSize); } if (!silent) { me.stop(); me.update(me.options.responsiveAnimationDuration); } return me; }, ensureScalesHaveIDs: function() { var options = this.options; var scalesOptions = options.scales || {}; var scaleOptions = options.scale; helpers.each(scalesOptions.xAxes, function(xAxisOptions, index) { xAxisOptions.id = xAxisOptions.id || ('x-axis-' + index); }); helpers.each(scalesOptions.yAxes, function(yAxisOptions, index) { yAxisOptions.id = yAxisOptions.id || ('y-axis-' + index); }); if (scaleOptions) { scaleOptions.id = scaleOptions.id || 'scale'; } }, /** * Builds a map of scale ID to scale object for future lookup. */ buildScales: function() { var me = this; var options = me.options; var scales = me.scales = {}; var items = []; if (options.scales) { items = items.concat( (options.scales.xAxes || []).map(function(xAxisOptions) { return { options: xAxisOptions, dtype: 'category' }; }), (options.scales.yAxes || []).map(function(yAxisOptions) { return { options: yAxisOptions, dtype: 'linear' }; })); } if (options.scale) { items.push({ options: options.scale, dtype: 'radialLinear', isDefault: true }); } helpers.each(items, function(item) { var scaleOptions = item.options; var scaleType = helpers.getValueOrDefault(scaleOptions.type, item.dtype); var scaleClass = Chart.scaleService.getScaleConstructor(scaleType); if (!scaleClass) { return; } var scale = new scaleClass({ id: scaleOptions.id, options: scaleOptions, ctx: me.chart.ctx, chart: me }); scales[scale.id] = scale; // TODO(SB): I think we should be able to remove this custom case (options.scale) // and consider it as a regular scale part of the "scales"" map only! This would // make the logic easier and remove some useless? custom code. if (item.isDefault) { me.scale = scale; } }); Chart.scaleService.addScalesToLayout(this); }, updateLayout: function() { Chart.layoutService.update(this, this.chart.width, this.chart.height); }, buildOrUpdateControllers: function() { var me = this; var types = []; var newControllers = []; helpers.each(me.data.datasets, function(dataset, datasetIndex) { var meta = me.getDatasetMeta(datasetIndex); if (!meta.type) { meta.type = dataset.type || me.config.type; } types.push(meta.type); if (meta.controller) { meta.controller.updateIndex(datasetIndex); } else { meta.controller = new Chart.controllers[meta.type](me, datasetIndex); newControllers.push(meta.controller); } }, me); if (types.length > 1) { for (var i = 1; i < types.length; i++) { if (types[i] !== types[i - 1]) { me.isCombo = true; break; } } } return newControllers; }, resetElements: function() { var me = this; helpers.each(me.data.datasets, function(dataset, datasetIndex) { me.getDatasetMeta(datasetIndex).controller.reset(); }, me); }, update: function update(animationDuration, lazy) { var me = this; Chart.plugins.notify('beforeUpdate', [me]); // In case the entire data object changed me.tooltip._data = me.data; // Make sure dataset controllers are updated and new controllers are reset var newControllers = me.buildOrUpdateControllers(); // Make sure all dataset controllers have correct meta data counts helpers.each(me.data.datasets, function(dataset, datasetIndex) { me.getDatasetMeta(datasetIndex).controller.buildOrUpdateElements(); }, me); Chart.layoutService.update(me, me.chart.width, me.chart.height); // Apply changes to the dataets that require the scales to have been calculated i.e BorderColor chages Chart.plugins.notify('afterScaleUpdate', [me]); // Can only reset the new controllers after the scales have been updated helpers.each(newControllers, function(controller) { controller.reset(); }); me.updateDatasets(); // Do this before render so that any plugins that need final scale updates can use it Chart.plugins.notify('afterUpdate', [me]); me.render(animationDuration, lazy); }, /** * @method beforeDatasetsUpdate * @description Called before all datasets are updated. If a plugin returns false, * the datasets update will be cancelled until another chart update is triggered. * @param {Object} instance the chart instance being updated. * @returns {Boolean} false to cancel the datasets update. * @memberof Chart.PluginBase * @since version 2.1.5 * @instance */ /** * @method afterDatasetsUpdate * @description Called after all datasets have been updated. Note that this * extension will not be called if the datasets update has been cancelled. * @param {Object} instance the chart instance being updated. * @memberof Chart.PluginBase * @since version 2.1.5 * @instance */ /** * Updates all datasets unless a plugin returns false to the beforeDatasetsUpdate * extension, in which case no datasets will be updated and the afterDatasetsUpdate * notification will be skipped. * @protected * @instance */ updateDatasets: function() { var me = this; var i, ilen; if (Chart.plugins.notify('beforeDatasetsUpdate', [ me ])) { for (i = 0, ilen = me.data.datasets.length; i < ilen; ++i) { me.getDatasetMeta(i).controller.update(); } Chart.plugins.notify('afterDatasetsUpdate', [ me ]); } }, render: function render(duration, lazy) { var me = this; Chart.plugins.notify('beforeRender', [me]); var animationOptions = me.options.animation; if (animationOptions && ((typeof duration !== 'undefined' && duration !== 0) || (typeof duration === 'undefined' && animationOptions.duration !== 0))) { var animation = new Chart.Animation(); animation.numSteps = (duration || animationOptions.duration) / 16.66; //60 fps animation.easing = animationOptions.easing; // render function animation.render = function(chartInstance, animationObject) { var easingFunction = helpers.easingEffects[animationObject.easing]; var stepDecimal = animationObject.currentStep / animationObject.numSteps; var easeDecimal = easingFunction(stepDecimal); chartInstance.draw(easeDecimal, stepDecimal, animationObject.currentStep); }; // user events animation.onAnimationProgress = animationOptions.onProgress; animation.onAnimationComplete = animationOptions.onComplete; Chart.animationService.addAnimation(me, animation, duration, lazy); } else { me.draw(); if (animationOptions && animationOptions.onComplete && animationOptions.onComplete.call) { animationOptions.onComplete.call(me); } } return me; }, draw: function(ease) { var me = this; var easingDecimal = ease || 1; me.clear(); Chart.plugins.notify('beforeDraw', [me, easingDecimal]); // Draw all the scales helpers.each(me.boxes, function(box) { box.draw(me.chartArea); }, me); if (me.scale) { me.scale.draw(); } Chart.plugins.notify('beforeDatasetsDraw', [me, easingDecimal]); // Draw each dataset via its respective controller (reversed to support proper line stacking) helpers.each(me.data.datasets, function(dataset, datasetIndex) { if (me.isDatasetVisible(datasetIndex)) { me.getDatasetMeta(datasetIndex).controller.draw(ease); } }, me, true); Chart.plugins.notify('afterDatasetsDraw', [me, easingDecimal]); // Finally draw the tooltip me.tooltip.transition(easingDecimal).draw(); Chart.plugins.notify('afterDraw', [me, easingDecimal]); }, // Get the single element that was clicked on // @return : An object containing the dataset index and element index of the matching element. Also contains the rectangle that was draw getElementAtEvent: function(e) { var me = this; var eventPosition = helpers.getRelativePosition(e, me.chart); var elementsArray = []; helpers.each(me.data.datasets, function(dataset, datasetIndex) { if (me.isDatasetVisible(datasetIndex)) { var meta = me.getDatasetMeta(datasetIndex); helpers.each(meta.data, function(element) { if (element.inRange(eventPosition.x, eventPosition.y)) { elementsArray.push(element); return elementsArray; } }); } }); return elementsArray.slice(0, 1); }, getElementsAtEvent: function(e) { var me = this; var eventPosition = helpers.getRelativePosition(e, me.chart); var elementsArray = []; var found = (function() { if (me.data.datasets) { for (var i = 0; i < me.data.datasets.length; i++) { var meta = me.getDatasetMeta(i); if (me.isDatasetVisible(i)) { for (var j = 0; j < meta.data.length; j++) { if (meta.data[j].inRange(eventPosition.x, eventPosition.y)) { return meta.data[j]; } } } } } }).call(me); if (!found) { return elementsArray; } helpers.each(me.data.datasets, function(dataset, datasetIndex) { if (me.isDatasetVisible(datasetIndex)) { var meta = me.getDatasetMeta(datasetIndex), element = meta.data[found._index]; if(element && !element._view.skip){ elementsArray.push(element); } } }, me); return elementsArray; }, getElementsAtXAxis: function(e) { var me = this; var eventPosition = helpers.getRelativePosition(e, me.chart); var elementsArray = []; var found = (function() { if (me.data.datasets) { for (var i = 0; i < me.data.datasets.length; i++) { var meta = me.getDatasetMeta(i); if (me.isDatasetVisible(i)) { for (var j = 0; j < meta.data.length; j++) { if (meta.data[j].inLabelRange(eventPosition.x, eventPosition.y)) { return meta.data[j]; } } } } } }).call(me); if (!found) { return elementsArray; } helpers.each(me.data.datasets, function(dataset, datasetIndex) { if (me.isDatasetVisible(datasetIndex)) { var meta = me.getDatasetMeta(datasetIndex); var index = helpers.findIndex(meta.data, function (it) { return found._model.x === it._model.x; }); if(index !== -1 && !meta.data[index]._view.skip) { elementsArray.push(meta.data[index]); } } }, me); return elementsArray; }, getElementsAtEventForMode: function(e, mode) { var me = this; switch (mode) { case 'single': return me.getElementAtEvent(e); case 'label': return me.getElementsAtEvent(e); case 'dataset': return me.getDatasetAtEvent(e); case 'x-axis': return me.getElementsAtXAxis(e); default: return e; } }, getDatasetAtEvent: function(e) { var elementsArray = this.getElementAtEvent(e); if (elementsArray.length > 0) { elementsArray = this.getDatasetMeta(elementsArray[0]._datasetIndex).data; } return elementsArray; }, getDatasetMeta: function(datasetIndex) { var me = this; var dataset = me.data.datasets[datasetIndex]; if (!dataset._meta) { dataset._meta = {}; } var meta = dataset._meta[me.id]; if (!meta) { meta = dataset._meta[me.id] = { type: null, data: [], dataset: null, controller: null, hidden: null, // See isDatasetVisible() comment xAxisID: null, yAxisID: null }; } return meta; }, getVisibleDatasetCount: function() { var count = 0; for (var i = 0, ilen = this.data.datasets.length; i