2015-06-15 03:15:10 +02:00
|
|
|
(function() {
|
|
|
|
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
//Declare root variable - window in the browser, global on the server
|
|
|
|
var root = this,
|
|
|
|
previous = root.Chart,
|
|
|
|
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 = {};
|
|
|
|
|
2015-06-15 22:36:02 +02:00
|
|
|
// Controllers available for dataset visualization eg. bar, line, slice, etc.
|
|
|
|
Chart.controllers = {};
|
|
|
|
|
|
|
|
// The main controller of a chart
|
2015-06-15 03:15:10 +02:00
|
|
|
Chart.Controller = function(instance) {
|
|
|
|
|
|
|
|
this.chart = instance;
|
2015-06-15 22:36:02 +02:00
|
|
|
this.config = instance.config;
|
|
|
|
this.data = this.config.data;
|
|
|
|
this.options = this.config.options = helpers.configMerge(Chart.defaults.global, Chart.defaults[this.config.type], this.config.options || {});
|
2015-06-15 03:15:10 +02:00
|
|
|
this.id = helpers.uid();
|
|
|
|
|
|
|
|
//Add the chart instance to the global namespace
|
|
|
|
Chart.instances[this.id] = this;
|
|
|
|
|
|
|
|
if (this.options.responsive) {
|
|
|
|
this.resize();
|
|
|
|
}
|
|
|
|
|
|
|
|
this.initialize.call(this);
|
|
|
|
|
|
|
|
return this;
|
|
|
|
};
|
|
|
|
|
|
|
|
helpers.extend(Chart.Controller.prototype, {
|
|
|
|
|
|
|
|
initialize: function initialize() {
|
2015-06-15 03:26:44 +02:00
|
|
|
|
2015-06-15 22:36:02 +02:00
|
|
|
// TODO
|
|
|
|
// If BeforeInit(this) doesn't return false, proceed
|
|
|
|
|
2015-06-15 03:26:44 +02:00
|
|
|
this.bindEvents();
|
2015-06-16 00:54:46 +02:00
|
|
|
|
|
|
|
// Make sure controllers are built first so that each dataset is bound to an axis before the scales
|
|
|
|
// are built
|
2015-06-15 22:36:02 +02:00
|
|
|
this.buildControllers();
|
2015-06-16 00:54:46 +02:00
|
|
|
this.buildScales();
|
2015-06-15 03:26:44 +02:00
|
|
|
this.resetElements();
|
|
|
|
this.initToolTip();
|
|
|
|
this.update();
|
|
|
|
|
2015-06-15 22:36:02 +02:00
|
|
|
// TODO
|
|
|
|
// If AfterInit(this) doesn't return false, proceed
|
|
|
|
|
2015-06-15 03:15:10 +02:00
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
|
|
|
clear: function clear() {
|
|
|
|
helpers.clear(this.chart);
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
|
|
|
stop: function stop() {
|
|
|
|
// Stops any current animation loop occuring
|
|
|
|
Chart.animationService.cancelAnimation(this);
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
|
|
|
resize: function resize() {
|
|
|
|
this.stop();
|
|
|
|
var canvas = this.chart.canvas,
|
|
|
|
newWidth = helpers.getMaximumWidth(this.chart.canvas),
|
2015-06-16 19:24:25 +02:00
|
|
|
newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : helpers.getMaximumHeight(this.chart.canvas);
|
2015-06-15 03:15:10 +02:00
|
|
|
|
|
|
|
canvas.width = this.chart.width = newWidth;
|
|
|
|
canvas.height = this.chart.height = newHeight;
|
|
|
|
|
|
|
|
helpers.retinaScale(this.chart);
|
|
|
|
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
2015-06-15 03:26:44 +02:00
|
|
|
buildScales: function buildScales() {
|
|
|
|
// Map of scale ID to scale object so we can lookup later
|
|
|
|
this.scales = {};
|
|
|
|
|
|
|
|
// Build the x axes
|
2015-06-16 03:14:11 +02:00
|
|
|
if (this.options.scales) {
|
|
|
|
if (this.options.scales.xAxes && this.options.scales.xAxes.length) {
|
|
|
|
helpers.each(this.options.scales.xAxes, function(xAxisOptions) {
|
|
|
|
var ScaleClass = Chart.scaleService.getScaleConstructor(xAxisOptions.type);
|
|
|
|
var scale = new ScaleClass({
|
|
|
|
ctx: this.chart.ctx,
|
|
|
|
options: xAxisOptions,
|
|
|
|
data: this.data,
|
|
|
|
id: xAxisOptions.id,
|
|
|
|
});
|
|
|
|
|
|
|
|
this.scales[scale.id] = scale;
|
|
|
|
}, this);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.options.scales.yAxes && this.options.scales.yAxes.length) {
|
|
|
|
// Build the y axes
|
|
|
|
helpers.each(this.options.scales.yAxes, function(yAxisOptions) {
|
|
|
|
var ScaleClass = Chart.scaleService.getScaleConstructor(yAxisOptions.type);
|
|
|
|
var scale = new ScaleClass({
|
|
|
|
ctx: this.chart.ctx,
|
|
|
|
options: yAxisOptions,
|
|
|
|
data: this.data,
|
|
|
|
id: yAxisOptions.id,
|
|
|
|
});
|
|
|
|
|
|
|
|
this.scales[scale.id] = scale;
|
|
|
|
}, this);
|
|
|
|
}
|
2015-06-16 08:17:26 +02:00
|
|
|
}
|
|
|
|
if (this.options.scale) {
|
|
|
|
// Build radial axes
|
|
|
|
var ScaleClass = Chart.scaleService.getScaleConstructor(this.options.scale.type);
|
|
|
|
var scale = new ScaleClass({
|
|
|
|
ctx: this.chart.ctx,
|
|
|
|
options: this.options.scale,
|
|
|
|
data: this.data,
|
|
|
|
chart: this.chart,
|
|
|
|
});
|
|
|
|
|
|
|
|
this.scale = scale;
|
2015-06-16 03:14:11 +02:00
|
|
|
}
|
2015-06-15 03:42:39 +02:00
|
|
|
|
|
|
|
Chart.scaleService.fitScalesForChart(this, this.chart.width, this.chart.height);
|
2015-06-15 03:26:44 +02:00
|
|
|
},
|
|
|
|
|
2015-06-15 22:36:02 +02:00
|
|
|
buildControllers: function() {
|
2015-06-16 00:18:56 +02:00
|
|
|
helpers.each(this.data.datasets, function(dataset, datasetIndex) {
|
2015-06-15 22:36:02 +02:00
|
|
|
var type = dataset.type || this.config.type;
|
|
|
|
if (dataset.controller) {
|
|
|
|
dataset.controller.updateIndex(datasetIndex);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
dataset.controller = new Chart.controllers[type](this, datasetIndex);
|
2015-06-16 00:18:56 +02:00
|
|
|
}, this);
|
2015-06-15 22:36:02 +02:00
|
|
|
},
|
|
|
|
|
2015-06-15 03:26:44 +02:00
|
|
|
resetElements: function resetElements() {
|
2015-06-16 00:18:56 +02:00
|
|
|
helpers.each(this.data.datasets, function(dataset, datasetIndex) {
|
2015-06-15 22:36:02 +02:00
|
|
|
dataset.controller.reset();
|
2015-06-16 00:18:56 +02:00
|
|
|
}, this);
|
2015-06-15 03:26:44 +02:00
|
|
|
},
|
2015-06-15 22:36:02 +02:00
|
|
|
|
|
|
|
|
2015-06-15 03:15:10 +02:00
|
|
|
update: function update(animationDuration) {
|
2015-06-15 03:26:44 +02:00
|
|
|
// This will loop through any data and do the appropriate element update for the type
|
2015-06-15 03:42:39 +02:00
|
|
|
Chart.scaleService.fitScalesForChart(this, this.chart.width, this.chart.height);
|
2015-06-16 00:18:56 +02:00
|
|
|
helpers.each(this.data.datasets, function(dataset, datasetIndex) {
|
2015-06-15 22:36:02 +02:00
|
|
|
dataset.controller.update();
|
2015-06-16 00:18:56 +02:00
|
|
|
}, this);
|
2015-06-15 03:42:39 +02:00
|
|
|
this.render(animationDuration);
|
2015-06-15 03:15:10 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
render: function render(duration) {
|
|
|
|
|
|
|
|
if (this.options.animation.duration !== 0 || duration) {
|
|
|
|
var animation = new Chart.Animation();
|
|
|
|
animation.numSteps = (duration || this.options.animation.duration) / 16.66; //60 fps
|
|
|
|
animation.easing = this.options.animation.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 = this.options.onAnimationProgress;
|
|
|
|
animation.onAnimationComplete = this.options.onAnimationComplete;
|
|
|
|
|
|
|
|
Chart.animationService.addAnimation(this, animation, duration);
|
|
|
|
} else {
|
|
|
|
this.draw();
|
|
|
|
this.options.onAnimationComplete.call(this);
|
|
|
|
}
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
2015-06-15 22:36:02 +02:00
|
|
|
draw: function(ease) {
|
|
|
|
var easingDecimal = ease || 1;
|
|
|
|
this.clear();
|
|
|
|
|
|
|
|
// Draw all the scales
|
|
|
|
helpers.each(this.scales, function(scale) {
|
|
|
|
scale.draw(this.chartArea);
|
|
|
|
}, this);
|
2015-06-16 08:17:26 +02:00
|
|
|
if (this.scale) {
|
|
|
|
this.scale.draw();
|
|
|
|
}
|
2015-06-15 22:36:02 +02:00
|
|
|
|
2015-06-16 19:37:07 +02:00
|
|
|
// Draw each dataset via its respective controller (reversed to support proper line stacking)
|
2015-06-15 22:36:02 +02:00
|
|
|
helpers.each(this.data.datasets, function(dataset, datasetIndex) {
|
|
|
|
dataset.controller.draw(ease);
|
2015-06-16 19:37:07 +02:00
|
|
|
}, this, true);
|
2015-06-15 22:36:02 +02:00
|
|
|
|
|
|
|
// Finally draw the tooltip
|
|
|
|
this.tooltip.transition(easingDecimal).draw();
|
|
|
|
},
|
|
|
|
|
2015-06-15 23:27:56 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2015-06-15 03:26:44 +02:00
|
|
|
// 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
|
2015-06-16 00:18:56 +02:00
|
|
|
getElementAtEvent: function(e) {
|
2015-06-15 03:26:44 +02:00
|
|
|
|
|
|
|
var eventPosition = helpers.getRelativePosition(e);
|
2015-06-16 06:43:45 +02:00
|
|
|
var elementsArray = [];
|
2015-06-15 03:26:44 +02:00
|
|
|
|
2015-06-16 00:18:56 +02:00
|
|
|
helpers.each(this.data.datasets, function(dataset, datasetIndex) {
|
|
|
|
helpers.each(dataset.metaData, function(element, index) {
|
2015-06-16 06:43:45 +02:00
|
|
|
if (element.inRange(eventPosition.x, eventPosition.y)) {
|
|
|
|
elementsArray.push(element);
|
|
|
|
return elementsArray;
|
2015-06-15 03:26:44 +02:00
|
|
|
}
|
2015-06-16 00:18:56 +02:00
|
|
|
}, this);
|
|
|
|
}, this);
|
2015-06-15 03:26:44 +02:00
|
|
|
|
2015-06-16 06:43:45 +02:00
|
|
|
return elementsArray;
|
2015-06-15 03:26:44 +02:00
|
|
|
},
|
|
|
|
|
2015-06-16 00:18:56 +02:00
|
|
|
getElementsAtEvent: function(e) {
|
|
|
|
var eventPosition = helpers.getRelativePosition(e);
|
|
|
|
var elementsArray = [];
|
2015-06-15 03:15:10 +02:00
|
|
|
|
2015-06-16 00:18:56 +02:00
|
|
|
helpers.each(this.data.datasets, function(dataset, datasetIndex) {
|
|
|
|
helpers.each(dataset.metaData, function(element, index) {
|
2015-06-16 01:21:13 +02:00
|
|
|
if (element.inLabelRange(eventPosition.x, eventPosition.y)) {
|
2015-06-16 00:18:56 +02:00
|
|
|
elementsArray.push(element);
|
2015-06-15 03:15:10 +02:00
|
|
|
}
|
2015-06-16 00:18:56 +02:00
|
|
|
}, this);
|
|
|
|
}, this);
|
2015-06-15 03:15:10 +02:00
|
|
|
|
2015-06-16 00:18:56 +02:00
|
|
|
return elementsArray;
|
2015-06-15 03:15:10 +02:00
|
|
|
},
|
|
|
|
|
2015-06-16 00:18:56 +02:00
|
|
|
getDatasetAtEvent: function(e) {
|
|
|
|
var eventPosition = helpers.getRelativePosition(e);
|
|
|
|
var elementsArray = [];
|
2015-06-15 03:42:39 +02:00
|
|
|
|
2015-06-16 00:18:56 +02:00
|
|
|
for (var datasetIndex = 0; datasetIndex < this.chart.data.datasets.length; datasetIndex++) {
|
|
|
|
for (elementIndex = 0; elementIndex < this.chart.data.datasets[datasetIndex].metaData.length; elementIndex++) {
|
|
|
|
if (this.chart.data.datasets[datasetIndex].metaData[elementIndex].inLabelRange(eventPosition.x, eventPosition.y)) {
|
|
|
|
helpers.each(this.chart.data.datasets, datasetIterator);
|
2015-06-15 03:42:39 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return elementsArray.length ? elementsArray : [];
|
|
|
|
},
|
|
|
|
|
2015-06-15 03:15:10 +02:00
|
|
|
generateLegend: function generateLegend() {
|
|
|
|
return template(this.options.legendTemplate, this);
|
|
|
|
},
|
|
|
|
|
|
|
|
destroy: function destroy() {
|
|
|
|
this.clear();
|
2015-06-15 03:26:44 +02:00
|
|
|
helpers.unbindEvents(this, this.events);
|
2015-06-15 03:15:10 +02:00
|
|
|
var canvas = this.chart.canvas;
|
|
|
|
|
|
|
|
// Reset canvas height/width attributes starts a fresh with the canvas context
|
|
|
|
canvas.width = this.chart.width;
|
|
|
|
canvas.height = this.chart.height;
|
|
|
|
|
|
|
|
// < IE9 doesn't support removeProperty
|
|
|
|
if (canvas.style.removeProperty) {
|
|
|
|
canvas.style.removeProperty('width');
|
|
|
|
canvas.style.removeProperty('height');
|
|
|
|
} else {
|
|
|
|
canvas.style.removeAttribute('width');
|
|
|
|
canvas.style.removeAttribute('height');
|
|
|
|
}
|
|
|
|
|
|
|
|
delete Chart.instances[this.id];
|
|
|
|
},
|
|
|
|
|
|
|
|
toBase64Image: function toBase64Image() {
|
|
|
|
return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments);
|
|
|
|
},
|
2015-06-15 03:42:39 +02:00
|
|
|
|
|
|
|
initToolTip: function initToolTip() {
|
|
|
|
this.tooltip = new Chart.Tooltip({
|
|
|
|
_chart: this.chart,
|
|
|
|
_data: this.data,
|
|
|
|
_options: this.options,
|
|
|
|
}, this);
|
|
|
|
},
|
|
|
|
|
2015-06-15 03:15:10 +02:00
|
|
|
bindEvents: function bindEvents() {
|
|
|
|
helpers.bindEvents(this, this.options.events, function(evt) {
|
2015-06-15 03:42:39 +02:00
|
|
|
this.eventHandler(evt);
|
2015-06-15 03:15:10 +02:00
|
|
|
});
|
|
|
|
},
|
|
|
|
eventHandler: function eventHandler(e) {
|
|
|
|
this.lastActive = this.lastActive || [];
|
|
|
|
|
|
|
|
// Find Active Elements
|
|
|
|
if (e.type == 'mouseout') {
|
|
|
|
this.active = [];
|
|
|
|
} else {
|
|
|
|
this.active = function() {
|
|
|
|
switch (this.options.hover.mode) {
|
|
|
|
case 'single':
|
2015-06-16 00:18:56 +02:00
|
|
|
return this.getElementAtEvent(e);
|
2015-06-15 03:15:10 +02:00
|
|
|
case 'label':
|
2015-06-16 00:18:56 +02:00
|
|
|
return this.getElementsAtEvent(e);
|
2015-06-15 03:15:10 +02:00
|
|
|
case 'dataset':
|
2015-06-16 00:18:56 +02:00
|
|
|
return this.getDatasetAtEvent(e);
|
2015-06-15 03:15:10 +02:00
|
|
|
default:
|
|
|
|
return e;
|
|
|
|
}
|
|
|
|
}.call(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
// On Hover hook
|
|
|
|
if (this.options.hover.onHover) {
|
|
|
|
this.options.hover.onHover.call(this, this.active);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (e.type == 'mouseup' || e.type == 'click') {
|
|
|
|
if (this.options.onClick) {
|
|
|
|
this.options.onClick.call(this, e, this.active);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var dataset;
|
|
|
|
var index;
|
|
|
|
// Remove styling for last active (even if it may still be active)
|
|
|
|
if (this.lastActive.length) {
|
|
|
|
switch (this.options.hover.mode) {
|
|
|
|
case 'single':
|
2015-06-16 00:18:56 +02:00
|
|
|
this.data.datasets[this.lastActive[0]._datasetIndex].controller.removeHoverStyle(this.lastActive[0], this.lastActive[0]._datasetIndex, this.lastActive[0]._index);
|
2015-06-15 03:15:10 +02:00
|
|
|
break;
|
|
|
|
case 'label':
|
|
|
|
for (var i = 0; i < this.lastActive.length; i++) {
|
2015-06-16 01:34:40 +02:00
|
|
|
this.data.datasets[this.lastActive[i]._datasetIndex].controller.removeHoverStyle(this.lastActive[i], this.lastActive[i]._datasetIndex, this.lastActive[i]._index);
|
2015-06-15 03:15:10 +02:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'dataset':
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
// Don't change anything
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Built in hover styling
|
|
|
|
if (this.active.length && this.options.hover.mode) {
|
|
|
|
switch (this.options.hover.mode) {
|
|
|
|
case 'single':
|
2015-06-16 00:18:56 +02:00
|
|
|
this.data.datasets[this.active[0]._datasetIndex].controller.setHoverStyle(this.active[0]);
|
2015-06-15 03:15:10 +02:00
|
|
|
break;
|
|
|
|
case 'label':
|
|
|
|
for (var i = 0; i < this.active.length; i++) {
|
2015-06-16 01:34:40 +02:00
|
|
|
this.data.datasets[this.active[i]._datasetIndex].controller.setHoverStyle(this.active[i]);
|
2015-06-15 03:15:10 +02:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'dataset':
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
// Don't change anything
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Built in Tooltips
|
|
|
|
if (this.options.tooltips.enabled) {
|
|
|
|
|
|
|
|
// The usual updates
|
|
|
|
this.tooltip.initialize();
|
|
|
|
|
|
|
|
// Active
|
|
|
|
if (this.active.length) {
|
|
|
|
this.tooltip._model.opacity = 1;
|
|
|
|
|
|
|
|
helpers.extend(this.tooltip, {
|
|
|
|
_active: this.active,
|
|
|
|
});
|
|
|
|
|
|
|
|
this.tooltip.update();
|
|
|
|
} else {
|
|
|
|
// Inactive
|
|
|
|
this.tooltip._model.opacity = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Hover animations
|
|
|
|
this.tooltip.pivot();
|
|
|
|
|
|
|
|
if (!this.animating) {
|
|
|
|
var changed;
|
|
|
|
|
|
|
|
helpers.each(this.active, function(element, index) {
|
|
|
|
if (element !== this.lastActive[index]) {
|
|
|
|
changed = true;
|
|
|
|
}
|
|
|
|
}, this);
|
|
|
|
|
|
|
|
// If entering, leaving, or changing elements, animate the change via pivot
|
|
|
|
if ((!this.lastActive.length && this.active.length) ||
|
|
|
|
(this.lastActive.length && !this.active.length) ||
|
|
|
|
(this.lastActive.length && this.active.length && changed)) {
|
|
|
|
|
|
|
|
this.stop();
|
|
|
|
this.render(this.options.hover.animationDuration);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remember Last Active
|
|
|
|
this.lastActive = this.active;
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
}).call(this);
|