mirror of
https://github.com/chartjs/Chart.js.git
synced 2024-10-07 20:59:08 +02:00
4741c6f09d
In order to take full advantage of the new plugin hooks called before and after a dataset is drawn, all drawing operations must happen on stable meta data, so make sure that transitions are performed before.
849 lines
22 KiB
JavaScript
849 lines
22 KiB
JavaScript
'use strict';
|
|
|
|
module.exports = function(Chart) {
|
|
|
|
var helpers = Chart.helpers;
|
|
var plugins = Chart.plugins;
|
|
var platform = Chart.platform;
|
|
|
|
// 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 = {};
|
|
|
|
/**
|
|
* Initializes the given config with global and chart default values.
|
|
*/
|
|
function initConfig(config) {
|
|
config = config || {};
|
|
|
|
// Do NOT use configMerge() for the data object because this method merges arrays
|
|
// and so would change references to labels and datasets, preventing data updates.
|
|
var data = config.data = config.data || {};
|
|
data.datasets = data.datasets || [];
|
|
data.labels = data.labels || [];
|
|
|
|
config.options = helpers.configMerge(
|
|
Chart.defaults.global,
|
|
Chart.defaults[config.type],
|
|
config.options || {});
|
|
|
|
return config;
|
|
}
|
|
|
|
/**
|
|
* Updates the config of the chart
|
|
* @param chart {Chart} chart to update the options for
|
|
*/
|
|
function updateConfig(chart) {
|
|
var newOptions = chart.options;
|
|
|
|
// Update Scale(s) with options
|
|
if (newOptions.scale) {
|
|
chart.scale.options = newOptions.scale;
|
|
} else if (newOptions.scales) {
|
|
newOptions.scales.xAxes.concat(newOptions.scales.yAxes).forEach(function(scaleOptions) {
|
|
chart.scales[scaleOptions.id].options = scaleOptions;
|
|
});
|
|
}
|
|
|
|
// Tooltip
|
|
chart.tooltip._options = newOptions.tooltips;
|
|
}
|
|
|
|
function positionIsHorizontal(position) {
|
|
return position === 'top' || position === 'bottom';
|
|
}
|
|
|
|
helpers.extend(Chart.prototype, /** @lends Chart */ {
|
|
/**
|
|
* @private
|
|
*/
|
|
construct: function(item, config) {
|
|
var me = this;
|
|
|
|
config = initConfig(config);
|
|
|
|
var context = platform.acquireContext(item, config);
|
|
var canvas = context && context.canvas;
|
|
var height = canvas && canvas.height;
|
|
var width = canvas && canvas.width;
|
|
|
|
me.id = helpers.uid();
|
|
me.ctx = context;
|
|
me.canvas = canvas;
|
|
me.config = config;
|
|
me.width = width;
|
|
me.height = height;
|
|
me.aspectRatio = height? width / height : null;
|
|
me.options = config.options;
|
|
me._bufferedRender = false;
|
|
|
|
/**
|
|
* Provided for backward compatibility, Chart and Chart.Controller have been merged,
|
|
* the "instance" still need to be defined since it might be called from plugins.
|
|
* @prop Chart#chart
|
|
* @deprecated since version 2.6.0
|
|
* @todo remove at version 3
|
|
* @private
|
|
*/
|
|
me.chart = me;
|
|
me.controller = me; // chart.chart.controller #inception
|
|
|
|
// Add the chart instance to the global namespace
|
|
Chart.instances[me.id] = me;
|
|
|
|
// Define alias to the config data: `chart.data === chart.config.data`
|
|
Object.defineProperty(me, 'data', {
|
|
get: function() {
|
|
return me.config.data;
|
|
},
|
|
set: function(value) {
|
|
me.config.data = value;
|
|
}
|
|
});
|
|
|
|
if (!context || !canvas) {
|
|
// The given item is not a compatible context2d element, let's return before finalizing
|
|
// the chart initialization but after setting basic chart / controller properties that
|
|
// can help to figure out that the chart is not valid (e.g chart.canvas !== null);
|
|
// https://github.com/chartjs/Chart.js/issues/2807
|
|
console.error("Failed to create chart: can't acquire context from the given item");
|
|
return;
|
|
}
|
|
|
|
me.initialize();
|
|
me.update();
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
initialize: function() {
|
|
var me = this;
|
|
|
|
// Before init plugin notification
|
|
plugins.notify(me, 'beforeInit');
|
|
|
|
helpers.retinaScale(me);
|
|
|
|
me.bindEvents();
|
|
|
|
if (me.options.responsive) {
|
|
// Initial resize before chart draws (must be silent to preserve initial animations).
|
|
me.resize(true);
|
|
}
|
|
|
|
// Make sure scales have IDs and are built before we build any controllers.
|
|
me.ensureScalesHaveIDs();
|
|
me.buildScales();
|
|
me.initToolTip();
|
|
|
|
// After init plugin notification
|
|
plugins.notify(me, 'afterInit');
|
|
|
|
return me;
|
|
},
|
|
|
|
clear: function() {
|
|
helpers.clear(this);
|
|
return this;
|
|
},
|
|
|
|
stop: function() {
|
|
// Stops any current animation loop occurring
|
|
Chart.animationService.cancelAnimation(this);
|
|
return this;
|
|
},
|
|
|
|
resize: function(silent) {
|
|
var me = this;
|
|
var options = me.options;
|
|
var canvas = me.canvas;
|
|
var aspectRatio = (options.maintainAspectRatio && me.aspectRatio) || null;
|
|
|
|
// the canvas render width and height will be casted to integers so make sure that
|
|
// the canvas display style uses the same integer values to avoid blurring effect.
|
|
var newWidth = Math.floor(helpers.getMaximumWidth(canvas));
|
|
var newHeight = Math.floor(aspectRatio? newWidth / aspectRatio : helpers.getMaximumHeight(canvas));
|
|
|
|
if (me.width === newWidth && me.height === newHeight) {
|
|
return;
|
|
}
|
|
|
|
canvas.width = me.width = newWidth;
|
|
canvas.height = me.height = newHeight;
|
|
canvas.style.width = newWidth + 'px';
|
|
canvas.style.height = newHeight + 'px';
|
|
|
|
helpers.retinaScale(me);
|
|
|
|
if (!silent) {
|
|
// Notify any plugins about the resize
|
|
var newSize = {width: newWidth, height: newHeight};
|
|
plugins.notify(me, 'resize', [newSize]);
|
|
|
|
// Notify of resize
|
|
if (me.options.onResize) {
|
|
me.options.onResize(me, newSize);
|
|
}
|
|
|
|
me.stop();
|
|
me.update(me.options.responsiveAnimationDuration);
|
|
}
|
|
},
|
|
|
|
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', dposition: 'bottom'};
|
|
}),
|
|
(options.scales.yAxes || []).map(function(yAxisOptions) {
|
|
return {options: yAxisOptions, dtype: 'linear', dposition: 'left'};
|
|
})
|
|
);
|
|
}
|
|
|
|
if (options.scale) {
|
|
items.push({
|
|
options: options.scale,
|
|
dtype: 'radialLinear',
|
|
isDefault: true,
|
|
dposition: 'chartArea'
|
|
});
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (positionIsHorizontal(scaleOptions.position) !== positionIsHorizontal(item.dposition)) {
|
|
scaleOptions.position = item.dposition;
|
|
}
|
|
|
|
var scale = new scaleClass({
|
|
id: scaleOptions.id,
|
|
options: scaleOptions,
|
|
ctx: me.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);
|
|
},
|
|
|
|
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;
|
|
},
|
|
|
|
/**
|
|
* Reset the elements of all datasets
|
|
* @private
|
|
*/
|
|
resetElements: function() {
|
|
var me = this;
|
|
helpers.each(me.data.datasets, function(dataset, datasetIndex) {
|
|
me.getDatasetMeta(datasetIndex).controller.reset();
|
|
}, me);
|
|
},
|
|
|
|
/**
|
|
* Resets the chart back to it's state before the initial animation
|
|
*/
|
|
reset: function() {
|
|
this.resetElements();
|
|
this.tooltip.initialize();
|
|
},
|
|
|
|
update: function(animationDuration, lazy) {
|
|
var me = this;
|
|
|
|
updateConfig(me);
|
|
|
|
if (plugins.notify(me, 'beforeUpdate') === false) {
|
|
return;
|
|
}
|
|
|
|
// 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);
|
|
|
|
me.updateLayout();
|
|
|
|
// 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
|
|
plugins.notify(me, 'afterUpdate');
|
|
|
|
if (me._bufferedRender) {
|
|
me._bufferedRequest = {
|
|
lazy: lazy,
|
|
duration: animationDuration
|
|
};
|
|
} else {
|
|
me.render(animationDuration, lazy);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Updates the chart layout unless a plugin returns `false` to the `beforeLayout`
|
|
* hook, in which case, plugins will not be called on `afterLayout`.
|
|
* @private
|
|
*/
|
|
updateLayout: function() {
|
|
var me = this;
|
|
|
|
if (plugins.notify(me, 'beforeLayout') === false) {
|
|
return;
|
|
}
|
|
|
|
Chart.layoutService.update(this, this.width, this.height);
|
|
|
|
/**
|
|
* Provided for backward compatibility, use `afterLayout` instead.
|
|
* @method IPlugin#afterScaleUpdate
|
|
* @deprecated since version 2.5.0
|
|
* @todo remove at version 3
|
|
* @private
|
|
*/
|
|
plugins.notify(me, 'afterScaleUpdate');
|
|
plugins.notify(me, 'afterLayout');
|
|
},
|
|
|
|
/**
|
|
* Updates all datasets unless a plugin returns `false` to the `beforeDatasetsUpdate`
|
|
* hook, in which case, plugins will not be called on `afterDatasetsUpdate`.
|
|
* @private
|
|
*/
|
|
updateDatasets: function() {
|
|
var me = this;
|
|
|
|
if (plugins.notify(me, 'beforeDatasetsUpdate') === false) {
|
|
return;
|
|
}
|
|
|
|
for (var i = 0, ilen = me.data.datasets.length; i < ilen; ++i) {
|
|
me.updateDataset(i);
|
|
}
|
|
|
|
plugins.notify(me, 'afterDatasetsUpdate');
|
|
},
|
|
|
|
/**
|
|
* Updates dataset at index unless a plugin returns `false` to the `beforeDatasetUpdate`
|
|
* hook, in which case, plugins will not be called on `afterDatasetUpdate`.
|
|
* @private
|
|
*/
|
|
updateDataset: function(index) {
|
|
var me = this;
|
|
var meta = me.getDatasetMeta(index);
|
|
var args = {
|
|
meta: meta,
|
|
index: index
|
|
};
|
|
|
|
if (plugins.notify(me, 'beforeDatasetUpdate', [args]) === false) {
|
|
return;
|
|
}
|
|
|
|
meta.controller.update();
|
|
|
|
plugins.notify(me, 'afterDatasetUpdate', [args]);
|
|
},
|
|
|
|
render: function(duration, lazy) {
|
|
var me = this;
|
|
|
|
if (plugins.notify(me, 'beforeRender') === false) {
|
|
return;
|
|
}
|
|
|
|
var animationOptions = me.options.animation;
|
|
var onComplete = function() {
|
|
plugins.notify(me, 'afterRender');
|
|
var callback = animationOptions && animationOptions.onComplete;
|
|
if (callback && callback.call) {
|
|
callback.call(me);
|
|
}
|
|
};
|
|
|
|
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(chart, animationObject) {
|
|
var easingFunction = helpers.easingEffects[animationObject.easing];
|
|
var stepDecimal = animationObject.currentStep / animationObject.numSteps;
|
|
var easeDecimal = easingFunction(stepDecimal);
|
|
|
|
chart.draw(easeDecimal, stepDecimal, animationObject.currentStep);
|
|
};
|
|
|
|
// user events
|
|
animation.onAnimationProgress = animationOptions.onProgress;
|
|
animation.onAnimationComplete = onComplete;
|
|
|
|
Chart.animationService.addAnimation(me, animation, duration, lazy);
|
|
} else {
|
|
me.draw();
|
|
onComplete();
|
|
}
|
|
|
|
return me;
|
|
},
|
|
|
|
draw: function(easingValue) {
|
|
var me = this;
|
|
|
|
me.clear();
|
|
|
|
if (easingValue === undefined || easingValue === null) {
|
|
easingValue = 1;
|
|
}
|
|
|
|
me.transition(easingValue);
|
|
|
|
if (plugins.notify(me, 'beforeDraw', [easingValue]) === false) {
|
|
return;
|
|
}
|
|
|
|
// Draw all the scales
|
|
helpers.each(me.boxes, function(box) {
|
|
box.draw(me.chartArea);
|
|
}, me);
|
|
|
|
if (me.scale) {
|
|
me.scale.draw();
|
|
}
|
|
|
|
me.drawDatasets(easingValue);
|
|
|
|
// Finally draw the tooltip
|
|
me.tooltip.draw();
|
|
|
|
plugins.notify(me, 'afterDraw', [easingValue]);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
transition: function(easingValue) {
|
|
var me = this;
|
|
|
|
for (var i=0, ilen=(me.data.datasets || []).length; i<ilen; ++i) {
|
|
if (me.isDatasetVisible(i)) {
|
|
me.getDatasetMeta(i).controller.transition(easingValue);
|
|
}
|
|
}
|
|
|
|
me.tooltip.transition(easingValue);
|
|
},
|
|
|
|
/**
|
|
* Draws all datasets unless a plugin returns `false` to the `beforeDatasetsDraw`
|
|
* hook, in which case, plugins will not be called on `afterDatasetsDraw`.
|
|
* @private
|
|
*/
|
|
drawDatasets: function(easingValue) {
|
|
var me = this;
|
|
|
|
if (plugins.notify(me, 'beforeDatasetsDraw', [easingValue]) === false) {
|
|
return;
|
|
}
|
|
|
|
// Draw datasets reversed to support proper line stacking
|
|
for (var i=(me.data.datasets || []).length - 1; i >= 0; --i) {
|
|
if (me.isDatasetVisible(i)) {
|
|
me.drawDataset(i, easingValue);
|
|
}
|
|
}
|
|
|
|
plugins.notify(me, 'afterDatasetsDraw', [easingValue]);
|
|
},
|
|
|
|
/**
|
|
* Draws dataset at index unless a plugin returns `false` to the `beforeDatasetDraw`
|
|
* hook, in which case, plugins will not be called on `afterDatasetDraw`.
|
|
* @private
|
|
*/
|
|
drawDataset: function(index, easingValue) {
|
|
var me = this;
|
|
var meta = me.getDatasetMeta(index);
|
|
var args = {
|
|
meta: meta,
|
|
index: index,
|
|
easingValue: easingValue
|
|
};
|
|
|
|
if (plugins.notify(me, 'beforeDatasetDraw', [args]) === false) {
|
|
return;
|
|
}
|
|
|
|
meta.controller.draw(easingValue);
|
|
|
|
plugins.notify(me, 'afterDatasetDraw', [args]);
|
|
},
|
|
|
|
// 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) {
|
|
return Chart.Interaction.modes.single(this, e);
|
|
},
|
|
|
|
getElementsAtEvent: function(e) {
|
|
return Chart.Interaction.modes.label(this, e, {intersect: true});
|
|
},
|
|
|
|
getElementsAtXAxis: function(e) {
|
|
return Chart.Interaction.modes['x-axis'](this, e, {intersect: true});
|
|
},
|
|
|
|
getElementsAtEventForMode: function(e, mode, options) {
|
|
var method = Chart.Interaction.modes[mode];
|
|
if (typeof method === 'function') {
|
|
return method(this, e, options);
|
|
}
|
|
|
|
return [];
|
|
},
|
|
|
|
getDatasetAtEvent: function(e) {
|
|
return Chart.Interaction.modes.dataset(this, e, {intersect: true});
|
|
},
|
|
|
|
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<ilen; ++i) {
|
|
if (this.isDatasetVisible(i)) {
|
|
count++;
|
|
}
|
|
}
|
|
return count;
|
|
},
|
|
|
|
isDatasetVisible: function(datasetIndex) {
|
|
var meta = this.getDatasetMeta(datasetIndex);
|
|
|
|
// meta.hidden is a per chart dataset hidden flag override with 3 states: if true or false,
|
|
// the dataset.hidden value is ignored, else if null, the dataset hidden state is returned.
|
|
return typeof meta.hidden === 'boolean'? !meta.hidden : !this.data.datasets[datasetIndex].hidden;
|
|
},
|
|
|
|
generateLegend: function() {
|
|
return this.options.legendCallback(this);
|
|
},
|
|
|
|
destroy: function() {
|
|
var me = this;
|
|
var canvas = me.canvas;
|
|
var meta, i, ilen;
|
|
|
|
me.stop();
|
|
|
|
// dataset controllers need to cleanup associated data
|
|
for (i = 0, ilen = me.data.datasets.length; i < ilen; ++i) {
|
|
meta = me.getDatasetMeta(i);
|
|
if (meta.controller) {
|
|
meta.controller.destroy();
|
|
meta.controller = null;
|
|
}
|
|
}
|
|
|
|
if (canvas) {
|
|
me.unbindEvents();
|
|
helpers.clear(me);
|
|
platform.releaseContext(me.ctx);
|
|
me.canvas = null;
|
|
me.ctx = null;
|
|
}
|
|
|
|
plugins.notify(me, 'destroy');
|
|
|
|
delete Chart.instances[me.id];
|
|
},
|
|
|
|
toBase64Image: function() {
|
|
return this.canvas.toDataURL.apply(this.canvas, arguments);
|
|
},
|
|
|
|
initToolTip: function() {
|
|
var me = this;
|
|
me.tooltip = new Chart.Tooltip({
|
|
_chart: me,
|
|
_chartInstance: me, // deprecated, backward compatibility
|
|
_data: me.data,
|
|
_options: me.options.tooltips
|
|
}, me);
|
|
me.tooltip.initialize();
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
bindEvents: function() {
|
|
var me = this;
|
|
var listeners = me._listeners = {};
|
|
var listener = function() {
|
|
me.eventHandler.apply(me, arguments);
|
|
};
|
|
|
|
helpers.each(me.options.events, function(type) {
|
|
platform.addEventListener(me, type, listener);
|
|
listeners[type] = listener;
|
|
});
|
|
|
|
// Responsiveness is currently based on the use of an iframe, however this method causes
|
|
// performance issues and could be troublesome when used with ad blockers. So make sure
|
|
// that the user is still able to create a chart without iframe when responsive is false.
|
|
// See https://github.com/chartjs/Chart.js/issues/2210
|
|
if (me.options.responsive) {
|
|
listener = function() {
|
|
me.resize();
|
|
};
|
|
|
|
platform.addEventListener(me, 'resize', listener);
|
|
listeners.resize = listener;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
unbindEvents: function() {
|
|
var me = this;
|
|
var listeners = me._listeners;
|
|
if (!listeners) {
|
|
return;
|
|
}
|
|
|
|
delete me._listeners;
|
|
helpers.each(listeners, function(listener, type) {
|
|
platform.removeEventListener(me, type, listener);
|
|
});
|
|
},
|
|
|
|
updateHoverStyle: function(elements, mode, enabled) {
|
|
var method = enabled? 'setHoverStyle' : 'removeHoverStyle';
|
|
var element, i, ilen;
|
|
|
|
for (i=0, ilen=elements.length; i<ilen; ++i) {
|
|
element = elements[i];
|
|
if (element) {
|
|
this.getDatasetMeta(element._datasetIndex).controller[method](element);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
eventHandler: function(e) {
|
|
var me = this;
|
|
var tooltip = me.tooltip;
|
|
|
|
if (plugins.notify(me, 'beforeEvent', [e]) === false) {
|
|
return;
|
|
}
|
|
|
|
// Buffer any update calls so that renders do not occur
|
|
me._bufferedRender = true;
|
|
me._bufferedRequest = null;
|
|
|
|
var changed = me.handleEvent(e);
|
|
changed |= tooltip && tooltip.handleEvent(e);
|
|
|
|
plugins.notify(me, 'afterEvent', [e]);
|
|
|
|
var bufferedRequest = me._bufferedRequest;
|
|
if (bufferedRequest) {
|
|
// If we have an update that was triggered, we need to do a normal render
|
|
me.render(bufferedRequest.duration, bufferedRequest.lazy);
|
|
} else if (changed && !me.animating) {
|
|
// If entering, leaving, or changing elements, animate the change via pivot
|
|
me.stop();
|
|
|
|
// We only need to render at this point. Updating will cause scales to be
|
|
// recomputed generating flicker & using more memory than necessary.
|
|
me.render(me.options.hover.animationDuration, true);
|
|
}
|
|
|
|
me._bufferedRender = false;
|
|
me._bufferedRequest = null;
|
|
|
|
return me;
|
|
},
|
|
|
|
/**
|
|
* Handle an event
|
|
* @private
|
|
* @param {IEvent} event the event to handle
|
|
* @return {Boolean} true if the chart needs to re-render
|
|
*/
|
|
handleEvent: function(e) {
|
|
var me = this;
|
|
var options = me.options || {};
|
|
var hoverOptions = options.hover;
|
|
var changed = false;
|
|
|
|
me.lastActive = me.lastActive || [];
|
|
|
|
// Find Active Elements for hover and tooltips
|
|
if (e.type === 'mouseout') {
|
|
me.active = [];
|
|
} else {
|
|
me.active = me.getElementsAtEventForMode(e, hoverOptions.mode, hoverOptions);
|
|
}
|
|
|
|
// On Hover hook
|
|
if (hoverOptions.onHover) {
|
|
// Need to call with native event here to not break backwards compatibility
|
|
hoverOptions.onHover.call(me, e.native, me.active);
|
|
}
|
|
|
|
if (e.type === 'mouseup' || e.type === 'click') {
|
|
if (options.onClick) {
|
|
// Use e.native here for backwards compatibility
|
|
options.onClick.call(me, e.native, me.active);
|
|
}
|
|
}
|
|
|
|
// Remove styling for last active (even if it may still be active)
|
|
if (me.lastActive.length) {
|
|
me.updateHoverStyle(me.lastActive, hoverOptions.mode, false);
|
|
}
|
|
|
|
// Built in hover styling
|
|
if (me.active.length && hoverOptions.mode) {
|
|
me.updateHoverStyle(me.active, hoverOptions.mode, true);
|
|
}
|
|
|
|
changed = !helpers.arrayEquals(me.active, me.lastActive);
|
|
|
|
// Remember Last Actives
|
|
me.lastActive = me.active;
|
|
|
|
return changed;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Provided for backward compatibility, use Chart instead.
|
|
* @class Chart.Controller
|
|
* @deprecated since version 2.6.0
|
|
* @todo remove at version 3
|
|
* @private
|
|
*/
|
|
Chart.Controller = Chart;
|
|
};
|