mirror of
https://github.com/chartjs/Chart.js.git
synced 2024-10-08 13:19:07 +02:00
f3457c9941
Dataset effective type is now stored under meta.type, allowing many charts to share the same dataset but with different types. Also move dataset.bar flag to meta.bar.
604 lines
17 KiB
JavaScript
604 lines
17 KiB
JavaScript
"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 = {};
|
|
|
|
// 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, {
|
|
|
|
initialize: function initialize() {
|
|
// Before init plugin notification
|
|
Chart.pluginService.notifyPlugins('beforeInit', [this]);
|
|
|
|
this.bindEvents();
|
|
|
|
// Make sure controllers are built first so that each dataset is bound to an axis before the scales
|
|
// are built
|
|
this.ensureScalesHaveIDs();
|
|
this.buildOrUpdateControllers();
|
|
this.buildScales();
|
|
this.buildSurroundingItems();
|
|
this.updateLayout();
|
|
this.resetElements();
|
|
this.initToolTip();
|
|
this.update();
|
|
|
|
// After init plugin notification
|
|
Chart.pluginService.notifyPlugins('afterInit', [this]);
|
|
|
|
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(silent) {
|
|
var canvas = this.chart.canvas;
|
|
var newWidth = helpers.getMaximumWidth(this.chart.canvas);
|
|
var newHeight = (this.options.maintainAspectRatio && isNaN(this.chart.aspectRatio) === false && isFinite(this.chart.aspectRatio) && this.chart.aspectRatio !== 0) ? newWidth / this.chart.aspectRatio : helpers.getMaximumHeight(this.chart.canvas);
|
|
|
|
var sizeChanged = this.chart.width !== newWidth || this.chart.height !== newHeight;
|
|
|
|
if (!sizeChanged)
|
|
return this;
|
|
|
|
canvas.width = this.chart.width = newWidth;
|
|
canvas.height = this.chart.height = newHeight;
|
|
|
|
helpers.retinaScale(this.chart);
|
|
|
|
if (!silent) {
|
|
this.stop();
|
|
this.update(this.options.responsiveAnimationDuration);
|
|
}
|
|
|
|
return this;
|
|
},
|
|
ensureScalesHaveIDs: function ensureScalesHaveIDs() {
|
|
var defaultXAxisID = 'x-axis-';
|
|
var defaultYAxisID = 'y-axis-';
|
|
|
|
if (this.options.scales) {
|
|
if (this.options.scales.xAxes && this.options.scales.xAxes.length) {
|
|
helpers.each(this.options.scales.xAxes, function(xAxisOptions, index) {
|
|
xAxisOptions.id = xAxisOptions.id || (defaultXAxisID + index);
|
|
});
|
|
}
|
|
|
|
if (this.options.scales.yAxes && this.options.scales.yAxes.length) {
|
|
// Build the y axes
|
|
helpers.each(this.options.scales.yAxes, function(yAxisOptions, index) {
|
|
yAxisOptions.id = yAxisOptions.id || (defaultYAxisID + index);
|
|
});
|
|
}
|
|
}
|
|
},
|
|
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, index) {
|
|
var xType = helpers.getValueOrDefault(xAxisOptions.type, 'category');
|
|
var ScaleClass = Chart.scaleService.getScaleConstructor(xType);
|
|
if (ScaleClass) {
|
|
var scale = new ScaleClass({
|
|
ctx: this.chart.ctx,
|
|
options: xAxisOptions,
|
|
chart: this,
|
|
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, index) {
|
|
var yType = helpers.getValueOrDefault(yAxisOptions.type, 'linear');
|
|
var ScaleClass = Chart.scaleService.getScaleConstructor(yType);
|
|
if (ScaleClass) {
|
|
var scale = new ScaleClass({
|
|
ctx: this.chart.ctx,
|
|
options: yAxisOptions,
|
|
chart: this,
|
|
id: yAxisOptions.id
|
|
});
|
|
|
|
this.scales[scale.id] = scale;
|
|
}
|
|
}, this);
|
|
}
|
|
}
|
|
if (this.options.scale) {
|
|
// Build radial axes
|
|
var ScaleClass = Chart.scaleService.getScaleConstructor(this.options.scale.type);
|
|
if (ScaleClass) {
|
|
var scale = new ScaleClass({
|
|
ctx: this.chart.ctx,
|
|
options: this.options.scale,
|
|
chart: this
|
|
});
|
|
|
|
this.scale = scale;
|
|
|
|
this.scales.radialScale = scale;
|
|
}
|
|
}
|
|
|
|
Chart.scaleService.addScalesToLayout(this);
|
|
},
|
|
|
|
buildSurroundingItems: function() {
|
|
if (this.options.title) {
|
|
this.titleBlock = new Chart.Title({
|
|
ctx: this.chart.ctx,
|
|
options: this.options.title,
|
|
chart: this
|
|
});
|
|
|
|
Chart.layoutService.addBox(this, this.titleBlock);
|
|
}
|
|
|
|
if (this.options.legend) {
|
|
this.legend = new Chart.Legend({
|
|
ctx: this.chart.ctx,
|
|
options: this.options.legend,
|
|
chart: this
|
|
});
|
|
|
|
Chart.layoutService.addBox(this, this.legend);
|
|
}
|
|
},
|
|
|
|
updateLayout: function() {
|
|
Chart.layoutService.update(this, this.chart.width, this.chart.height);
|
|
},
|
|
|
|
buildOrUpdateControllers: function buildOrUpdateControllers() {
|
|
var types = [];
|
|
var newControllers = [];
|
|
|
|
helpers.each(this.data.datasets, function(dataset, datasetIndex) {
|
|
var meta = this.getDatasetMeta(datasetIndex);
|
|
if (!meta.type) {
|
|
meta.type = dataset.type || this.config.type;
|
|
}
|
|
|
|
types.push(meta.type);
|
|
|
|
if (meta.controller) {
|
|
meta.controller.updateIndex(datasetIndex);
|
|
} else {
|
|
meta.controller = new Chart.controllers[meta.type](this, datasetIndex);
|
|
newControllers.push(meta.controller);
|
|
}
|
|
}, this);
|
|
|
|
if (types.length > 1) {
|
|
for (var i = 1; i < types.length; i++) {
|
|
if (types[i] !== types[i - 1]) {
|
|
this.isCombo = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return newControllers;
|
|
},
|
|
|
|
resetElements: function resetElements() {
|
|
helpers.each(this.data.datasets, function(dataset, datasetIndex) {
|
|
this.getDatasetMeta(datasetIndex).controller.reset();
|
|
}, this);
|
|
},
|
|
|
|
update: function update(animationDuration, lazy) {
|
|
Chart.pluginService.notifyPlugins('beforeUpdate', [this]);
|
|
|
|
// In case the entire data object changed
|
|
this.tooltip._data = this.data;
|
|
|
|
// Make sure dataset controllers are updated and new controllers are reset
|
|
var newControllers = this.buildOrUpdateControllers();
|
|
|
|
// Make sure all dataset controllers have correct meta data counts
|
|
helpers.each(this.data.datasets, function(dataset, datasetIndex) {
|
|
this.getDatasetMeta(datasetIndex).controller.buildOrUpdateElements();
|
|
}, this);
|
|
|
|
Chart.layoutService.update(this, this.chart.width, this.chart.height);
|
|
|
|
// Can only reset the new controllers after the scales have been updated
|
|
helpers.each(newControllers, function(controller) {
|
|
controller.reset();
|
|
});
|
|
|
|
// This will loop through any data and do the appropriate element update for the type
|
|
helpers.each(this.data.datasets, function(dataset, datasetIndex) {
|
|
this.getDatasetMeta(datasetIndex).controller.update();
|
|
}, this);
|
|
|
|
this.render(animationDuration, lazy);
|
|
|
|
Chart.pluginService.notifyPlugins('afterUpdate', [this]);
|
|
},
|
|
|
|
render: function render(duration, lazy) {
|
|
Chart.pluginService.notifyPlugins('beforeRender', [this]);
|
|
|
|
if (this.options.animation && ((typeof duration !== 'undefined' && duration !== 0) || (typeof duration === 'undefined' && this.options.animation.duration !== 0))) {
|
|
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.animation.onProgress;
|
|
animation.onAnimationComplete = this.options.animation.onComplete;
|
|
|
|
Chart.animationService.addAnimation(this, animation, duration, lazy);
|
|
} else {
|
|
this.draw();
|
|
if (this.options.animation && this.options.animation.onComplete && this.options.animation.onComplete.call) {
|
|
this.options.animation.onComplete.call(this);
|
|
}
|
|
}
|
|
return this;
|
|
},
|
|
|
|
draw: function(ease) {
|
|
var easingDecimal = ease || 1;
|
|
this.clear();
|
|
|
|
Chart.pluginService.notifyPlugins('beforeDraw', [this, easingDecimal]);
|
|
|
|
// Draw all the scales
|
|
helpers.each(this.boxes, function(box) {
|
|
box.draw(this.chartArea);
|
|
}, this);
|
|
if (this.scale) {
|
|
this.scale.draw();
|
|
}
|
|
|
|
// Clip out the chart area so that anything outside does not draw. This is necessary for zoom and pan to function
|
|
this.chart.ctx.save();
|
|
this.chart.ctx.beginPath();
|
|
this.chart.ctx.rect(this.chartArea.left, this.chartArea.top, this.chartArea.right - this.chartArea.left, this.chartArea.bottom - this.chartArea.top);
|
|
this.chart.ctx.clip();
|
|
|
|
// Draw each dataset via its respective controller (reversed to support proper line stacking)
|
|
helpers.each(this.data.datasets, function(dataset, datasetIndex) {
|
|
if (helpers.isDatasetVisible(dataset)) {
|
|
this.getDatasetMeta(datasetIndex).controller.draw(ease);
|
|
}
|
|
}, this, true);
|
|
|
|
// Restore from the clipping operation
|
|
this.chart.ctx.restore();
|
|
|
|
// Finally draw the tooltip
|
|
this.tooltip.transition(easingDecimal).draw();
|
|
|
|
Chart.pluginService.notifyPlugins('afterDraw', [this, 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 eventPosition = helpers.getRelativePosition(e, this.chart);
|
|
var elementsArray = [];
|
|
|
|
helpers.each(this.data.datasets, function(dataset, datasetIndex) {
|
|
var meta = this.getDatasetMeta(datasetIndex);
|
|
if (helpers.isDatasetVisible(dataset)) {
|
|
helpers.each(meta.data, function(element, index) {
|
|
if (element.inRange(eventPosition.x, eventPosition.y)) {
|
|
elementsArray.push(element);
|
|
return elementsArray;
|
|
}
|
|
});
|
|
}
|
|
}, this);
|
|
|
|
return elementsArray;
|
|
},
|
|
|
|
getElementsAtEvent: function(e) {
|
|
var eventPosition = helpers.getRelativePosition(e, this.chart);
|
|
var elementsArray = [];
|
|
|
|
var found = (function() {
|
|
if (this.data.datasets) {
|
|
for (var i = 0; i < this.data.datasets.length; i++) {
|
|
var meta = this.getDatasetMeta(i);
|
|
if (helpers.isDatasetVisible(this.data.datasets[i])) {
|
|
for (var j = 0; j < meta.data.length; j++) {
|
|
if (meta.data[j].inRange(eventPosition.x, eventPosition.y)) {
|
|
return meta.data[j];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}).call(this);
|
|
|
|
if (!found) {
|
|
return elementsArray;
|
|
}
|
|
|
|
helpers.each(this.data.datasets, function(dataset, datasetIndex) {
|
|
var meta = this.getDatasetMeta(datasetIndex);
|
|
if (helpers.isDatasetVisible(dataset)) {
|
|
elementsArray.push(meta.data[found._index]);
|
|
}
|
|
}, this);
|
|
|
|
return elementsArray;
|
|
},
|
|
|
|
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 dataset = this.data.datasets[datasetIndex];
|
|
if (!dataset._meta) {
|
|
dataset._meta = {};
|
|
}
|
|
|
|
var meta = dataset._meta[this.id];
|
|
if (!meta) {
|
|
meta = dataset._meta[this.id] = {
|
|
type: null,
|
|
data: [],
|
|
dataset: null,
|
|
controller: null,
|
|
xAxisID: null,
|
|
yAxisID: null
|
|
};
|
|
}
|
|
|
|
return meta;
|
|
},
|
|
|
|
generateLegend: function generateLegend() {
|
|
return this.options.legendCallback(this);
|
|
},
|
|
|
|
destroy: function destroy() {
|
|
this.clear();
|
|
helpers.unbindEvents(this, this.events);
|
|
helpers.removeResizeListener(this.chart.canvas.parentNode);
|
|
|
|
// Reset canvas height/width attributes
|
|
var canvas = this.chart.canvas;
|
|
canvas.width = this.chart.width;
|
|
canvas.height = this.chart.height;
|
|
|
|
// if we scaled the canvas in response to a devicePixelRatio !== 1, we need to undo that transform here
|
|
if (this.chart.originalDevicePixelRatio !== undefined) {
|
|
this.chart.ctx.scale(1 / this.chart.originalDevicePixelRatio, 1 / this.chart.originalDevicePixelRatio);
|
|
}
|
|
|
|
// Reset to the old style since it may have been changed by the device pixel ratio changes
|
|
canvas.style.width = this.chart.originalCanvasStyleWidth;
|
|
canvas.style.height = this.chart.originalCanvasStyleHeight;
|
|
|
|
Chart.pluginService.notifyPlugins('destroy', [this]);
|
|
|
|
delete Chart.instances[this.id];
|
|
},
|
|
|
|
toBase64Image: function toBase64Image() {
|
|
return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments);
|
|
},
|
|
|
|
initToolTip: function initToolTip() {
|
|
this.tooltip = new Chart.Tooltip({
|
|
_chart: this.chart,
|
|
_chartInstance: this,
|
|
_data: this.data,
|
|
_options: this.options
|
|
}, this);
|
|
},
|
|
|
|
bindEvents: function bindEvents() {
|
|
helpers.bindEvents(this, this.options.events, function(evt) {
|
|
this.eventHandler(evt);
|
|
});
|
|
},
|
|
eventHandler: function eventHandler(e) {
|
|
this.lastActive = this.lastActive || [];
|
|
this.lastTooltipActive = this.lastTooltipActive || [];
|
|
|
|
// Find Active Elements for hover and tooltips
|
|
if (e.type === 'mouseout') {
|
|
this.active = [];
|
|
this.tooltipActive = [];
|
|
} else {
|
|
|
|
var _this = this;
|
|
var getItemsForMode = function(mode) {
|
|
switch (mode) {
|
|
case 'single':
|
|
return _this.getElementAtEvent(e);
|
|
case 'label':
|
|
return _this.getElementsAtEvent(e);
|
|
case 'dataset':
|
|
return _this.getDatasetAtEvent(e);
|
|
default:
|
|
return e;
|
|
}
|
|
};
|
|
|
|
this.active = getItemsForMode(this.options.hover.mode);
|
|
this.tooltipActive = getItemsForMode(this.options.tooltips.mode);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
if (this.legend && this.legend.handleEvent) {
|
|
this.legend.handleEvent(e);
|
|
}
|
|
}
|
|
|
|
// Remove styling for last active (even if it may still be active)
|
|
if (this.lastActive.length) {
|
|
switch (this.options.hover.mode) {
|
|
case 'single':
|
|
this.getDatasetMeta(this.lastActive[0]._datasetIndex).controller.removeHoverStyle(this.lastActive[0], this.lastActive[0]._datasetIndex, this.lastActive[0]._index);
|
|
break;
|
|
case 'label':
|
|
case 'dataset':
|
|
for (var i = 0; i < this.lastActive.length; i++) {
|
|
if (this.lastActive[i])
|
|
this.getDatasetMeta(this.lastActive[i]._datasetIndex).controller.removeHoverStyle(this.lastActive[i], this.lastActive[i]._datasetIndex, this.lastActive[i]._index);
|
|
}
|
|
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.getDatasetMeta(this.active[0]._datasetIndex).controller.setHoverStyle(this.active[0]);
|
|
break;
|
|
case 'label':
|
|
case 'dataset':
|
|
for (var j = 0; j < this.active.length; j++) {
|
|
if (this.active[j])
|
|
this.getDatasetMeta(this.active[j]._datasetIndex).controller.setHoverStyle(this.active[j]);
|
|
}
|
|
break;
|
|
default:
|
|
// Don't change anything
|
|
}
|
|
}
|
|
|
|
|
|
// Built in Tooltips
|
|
if (this.options.tooltips.enabled || this.options.tooltips.custom) {
|
|
|
|
// The usual updates
|
|
this.tooltip.initialize();
|
|
this.tooltip._active = this.tooltipActive;
|
|
this.tooltip.update();
|
|
}
|
|
|
|
// 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);
|
|
|
|
helpers.each(this.tooltipActive, function(element, index) {
|
|
if (element !== this.lastTooltipActive[index]) {
|
|
changed = true;
|
|
}
|
|
}, this);
|
|
|
|
// If entering, leaving, or changing elements, animate the change via pivot
|
|
if ((this.lastActive.length !== this.active.length) ||
|
|
(this.lastTooltipActive.length !== this.tooltipActive.length) ||
|
|
changed) {
|
|
|
|
this.stop();
|
|
|
|
if (this.options.tooltips.enabled || this.options.tooltips.custom) {
|
|
this.tooltip.update(true);
|
|
}
|
|
|
|
// We only need to render at this point. Updating will cause scales to be recomputed generating flicker & using more
|
|
// memory than necessary.
|
|
this.render(this.options.hover.animationDuration, true);
|
|
}
|
|
}
|
|
|
|
// Remember Last Actives
|
|
this.lastActive = this.active;
|
|
this.lastTooltipActive = this.tooltipActive;
|
|
return this;
|
|
}
|
|
});
|
|
};
|