Chart.js/src/core/core.controller.js

433 lines
12 KiB
JavaScript
Raw Normal View History

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 = {};
// 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;
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
// 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
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();
// 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),
newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : getMaximumHeight(this.chart.canvas);
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
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 07:54:25 +02:00
if (this.options.scale) {
// Build radial axes
var ScaleClass = Chart.scaleService.getScaleConstructor(axisOptions.type);
var scale = new ScaleClass({
ctx: this.chart.ctx,
options: axisOptions,
data: this.data,
id: axisOptions.id,
chart: this.chart,
});
this.scale = scale;
}
}
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
},
buildControllers: function() {
helpers.each(this.data.datasets, function(dataset, datasetIndex) {
var type = dataset.type || this.config.type;
if (dataset.controller) {
dataset.controller.updateIndex(datasetIndex);
return;
}
dataset.controller = new Chart.controllers[type](this, datasetIndex);
}, this);
},
2015-06-15 03:26:44 +02:00
resetElements: function resetElements() {
helpers.each(this.data.datasets, function(dataset, datasetIndex) {
dataset.controller.reset();
}, this);
2015-06-15 03:26:44 +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);
helpers.each(this.data.datasets, function(dataset, datasetIndex) {
dataset.controller.update();
}, 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;
},
draw: function(ease) {
var easingDecimal = ease || 1;
this.clear();
// Draw all the scales
helpers.each(this.scales, function(scale) {
scale.draw(this.chartArea);
}, this);
// Draw each dataset via its respective controller
// TODO: needs support for reverse stacking (line chart)
helpers.each(this.data.datasets, function(dataset, datasetIndex) {
dataset.controller.draw(ease);
}, this);
// Finally draw the tooltip
this.tooltip.transition(easingDecimal).draw();
},
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
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
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
}
}, 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
},
getElementsAtEvent: function(e) {
var eventPosition = helpers.getRelativePosition(e);
var elementsArray = [];
2015-06-15 03:15:10 +02:00
helpers.each(this.data.datasets, function(dataset, datasetIndex) {
helpers.each(dataset.metaData, function(element, index) {
if (element.inLabelRange(eventPosition.x, eventPosition.y)) {
elementsArray.push(element);
2015-06-15 03:15:10 +02:00
}
}, this);
}, this);
2015-06-15 03:15:10 +02:00
return elementsArray;
2015-06-15 03:15:10 +02:00
},
getDatasetAtEvent: function(e) {
var eventPosition = helpers.getRelativePosition(e);
var elementsArray = [];
2015-06-15 03:42:39 +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':
return this.getElementAtEvent(e);
2015-06-15 03:15:10 +02:00
case 'label':
return this.getElementsAtEvent(e);
2015-06-15 03:15:10 +02:00
case 'dataset':
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':
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':
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);