Make Chart.Animation/animations/Tooltip importable (#5382)

This commit is contained in:
Simon Brunel 2018-04-02 10:55:52 +02:00 committed by GitHub
parent 9fbac88938
commit d284686452
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 1045 additions and 991 deletions

View File

@ -1,5 +1,6 @@
{
"indent-style": "tabs",
"line-end-style": false,
"attr-quote-style": "double",
"spec-char-escape": false,
"attr-bans": [

View File

@ -8,6 +8,8 @@ Chart.helpers = require('./helpers/index');
// @todo dispatch these helpers into appropriated helpers/helpers.* file and write unit tests!
require('./core/core.helpers')(Chart);
Chart.Animation = require('./core/core.animation');
Chart.animationService = require('./core/core.animations');
Chart.defaults = require('./core/core.defaults');
Chart.Element = require('./core/core.element');
Chart.elements = require('./elements/index');
@ -16,13 +18,12 @@ Chart.layouts = require('./core/core.layouts');
Chart.platform = require('./platforms/platform');
Chart.plugins = require('./core/core.plugins');
Chart.Ticks = require('./core/core.ticks');
Chart.Tooltip = require('./core/core.tooltip');
require('./core/core.animation')(Chart);
require('./core/core.controller')(Chart);
require('./core/core.datasetController')(Chart);
require('./core/core.scaleService')(Chart);
require('./core/core.scale')(Chart);
require('./core/core.tooltip')(Chart);
require('./scales/scale.linearbase')(Chart);
require('./scales/scale.category')(Chart);

View File

@ -1,22 +1,8 @@
/* global window: false */
'use strict';
var defaults = require('./core.defaults');
var Element = require('./core.element');
var helpers = require('../helpers/index');
defaults._set('global', {
animation: {
duration: 1000,
easing: 'easeOutQuart',
onProgress: helpers.noop,
onComplete: helpers.noop
}
});
module.exports = function(Chart) {
Chart.Animation = Element.extend({
var exports = module.exports = Element.extend({
chart: null, // the animation associated chart instance
currentStep: 0, // the current animation step
numSteps: 60, // default number of steps
@ -25,148 +11,33 @@ module.exports = function(Chart) {
onAnimationProgress: null, // user specified callback to fire on each step of the animation
onAnimationComplete: null, // user specified callback to fire when the animation finishes
});
});
Chart.animationService = {
frameDuration: 17,
animations: [],
dropFrames: 0,
request: null,
// DEPRECATIONS
/**
* @param {Chart} chart - The chart to animate.
* @param {Chart.Animation} animation - The animation that we will animate.
* @param {Number} duration - The animation duration in ms.
* @param {Boolean} lazy - if true, the chart is not marked as animating to enable more responsive interactions
*/
addAnimation: function(chart, animation, duration, lazy) {
var animations = this.animations;
var i, ilen;
animation.chart = chart;
if (!lazy) {
chart.animating = true;
}
for (i = 0, ilen = animations.length; i < ilen; ++i) {
if (animations[i].chart === chart) {
animations[i] = animation;
return;
}
}
animations.push(animation);
// If there are no animations queued, manually kickstart a digest, for lack of a better word
if (animations.length === 1) {
this.requestAnimationFrame();
}
},
cancelAnimation: function(chart) {
var index = helpers.findIndex(this.animations, function(animation) {
return animation.chart === chart;
});
if (index !== -1) {
this.animations.splice(index, 1);
chart.animating = false;
}
},
requestAnimationFrame: function() {
var me = this;
if (me.request === null) {
// Skip animation frame requests until the active one is executed.
// This can happen when processing mouse events, e.g. 'mousemove'
// and 'mouseout' events will trigger multiple renders.
me.request = helpers.requestAnimFrame.call(window, function() {
me.request = null;
me.startDigest();
});
}
},
/**
* @private
*/
startDigest: function() {
var me = this;
var startTime = Date.now();
var framesToDrop = 0;
if (me.dropFrames > 1) {
framesToDrop = Math.floor(me.dropFrames);
me.dropFrames = me.dropFrames % 1;
}
me.advance(1 + framesToDrop);
var endTime = Date.now();
me.dropFrames += (endTime - startTime) / me.frameDuration;
// Do we have more stuff to animate?
if (me.animations.length > 0) {
me.requestAnimationFrame();
}
},
/**
* @private
*/
advance: function(count) {
var animations = this.animations;
var animation, chart;
var i = 0;
while (i < animations.length) {
animation = animations[i];
chart = animation.chart;
animation.currentStep = (animation.currentStep || 0) + count;
animation.currentStep = Math.min(animation.currentStep, animation.numSteps);
helpers.callback(animation.render, [chart, animation], chart);
helpers.callback(animation.onAnimationProgress, [animation], chart);
if (animation.currentStep >= animation.numSteps) {
helpers.callback(animation.onAnimationComplete, [animation], chart);
chart.animating = false;
animations.splice(i, 1);
} else {
++i;
}
}
}
};
/**
/**
* Provided for backward compatibility, use Chart.Animation instead
* @prop Chart.Animation#animationObject
* @deprecated since version 2.6.0
* @todo remove at version 3
*/
Object.defineProperty(Chart.Animation.prototype, 'animationObject', {
Object.defineProperty(exports.prototype, 'animationObject', {
get: function() {
return this;
}
});
});
/**
/**
* Provided for backward compatibility, use Chart.Animation#chart instead
* @prop Chart.Animation#chartInstance
* @deprecated since version 2.6.0
* @todo remove at version 3
*/
Object.defineProperty(Chart.Animation.prototype, 'chartInstance', {
Object.defineProperty(exports.prototype, 'chartInstance', {
get: function() {
return this.chart;
},
set: function(value) {
this.chart = value;
}
});
};
});

129
src/core/core.animations.js Normal file
View File

@ -0,0 +1,129 @@
/* global window: false */
'use strict';
var defaults = require('./core.defaults');
var helpers = require('../helpers/index');
defaults._set('global', {
animation: {
duration: 1000,
easing: 'easeOutQuart',
onProgress: helpers.noop,
onComplete: helpers.noop
}
});
module.exports = {
frameDuration: 17,
animations: [],
dropFrames: 0,
request: null,
/**
* @param {Chart} chart - The chart to animate.
* @param {Chart.Animation} animation - The animation that we will animate.
* @param {Number} duration - The animation duration in ms.
* @param {Boolean} lazy - if true, the chart is not marked as animating to enable more responsive interactions
*/
addAnimation: function(chart, animation, duration, lazy) {
var animations = this.animations;
var i, ilen;
animation.chart = chart;
if (!lazy) {
chart.animating = true;
}
for (i = 0, ilen = animations.length; i < ilen; ++i) {
if (animations[i].chart === chart) {
animations[i] = animation;
return;
}
}
animations.push(animation);
// If there are no animations queued, manually kickstart a digest, for lack of a better word
if (animations.length === 1) {
this.requestAnimationFrame();
}
},
cancelAnimation: function(chart) {
var index = helpers.findIndex(this.animations, function(animation) {
return animation.chart === chart;
});
if (index !== -1) {
this.animations.splice(index, 1);
chart.animating = false;
}
},
requestAnimationFrame: function() {
var me = this;
if (me.request === null) {
// Skip animation frame requests until the active one is executed.
// This can happen when processing mouse events, e.g. 'mousemove'
// and 'mouseout' events will trigger multiple renders.
me.request = helpers.requestAnimFrame.call(window, function() {
me.request = null;
me.startDigest();
});
}
},
/**
* @private
*/
startDigest: function() {
var me = this;
var startTime = Date.now();
var framesToDrop = 0;
if (me.dropFrames > 1) {
framesToDrop = Math.floor(me.dropFrames);
me.dropFrames = me.dropFrames % 1;
}
me.advance(1 + framesToDrop);
var endTime = Date.now();
me.dropFrames += (endTime - startTime) / me.frameDuration;
// Do we have more stuff to animate?
if (me.animations.length > 0) {
me.requestAnimationFrame();
}
},
/**
* @private
*/
advance: function(count) {
var animations = this.animations;
var animation, chart;
var i = 0;
while (i < animations.length) {
animation = animations[i];
chart = animation.chart;
animation.currentStep = (animation.currentStep || 0) + count;
animation.currentStep = Math.min(animation.currentStep, animation.numSteps);
helpers.callback(animation.render, [chart, animation], chart);
helpers.callback(animation.onAnimationProgress, [animation], chart);
if (animation.currentStep >= animation.numSteps) {
helpers.callback(animation.onAnimationComplete, [animation], chart);
chart.animating = false;
animations.splice(i, 1);
} else {
++i;
}
}
}
};

View File

@ -1,11 +1,14 @@
'use strict';
var Animation = require('./core.animation');
var animations = require('./core.animations');
var defaults = require('./core.defaults');
var helpers = require('../helpers/index');
var Interaction = require('./core.interaction');
var layouts = require('./core.layouts');
var platform = require('../platforms/platform');
var plugins = require('./core.plugins');
var Tooltip = require('./core.tooltip');
module.exports = function(Chart) {
@ -164,7 +167,7 @@ module.exports = function(Chart) {
stop: function() {
// Stops any current animation loop occurring
Chart.animationService.cancelAnimation(this);
animations.cancelAnimation(this);
return this;
},
@ -519,7 +522,7 @@ module.exports = function(Chart) {
};
if (animationOptions && ((typeof duration !== 'undefined' && duration !== 0) || (typeof duration === 'undefined' && animationOptions.duration !== 0))) {
var animation = new Chart.Animation({
var animation = new Animation({
numSteps: (duration || animationOptions.duration) / 16.66, // 60 fps
easing: config.easing || animationOptions.easing,
@ -535,12 +538,12 @@ module.exports = function(Chart) {
onAnimationComplete: onComplete
});
Chart.animationService.addAnimation(me, animation, duration, lazy);
animations.addAnimation(me, animation, duration, lazy);
} else {
me.draw();
// See https://github.com/chartjs/Chart.js/issues/3781
onComplete(new Chart.Animation({numSteps: 0, chart: me}));
onComplete(new Animation({numSteps: 0, chart: me}));
}
return me;
@ -775,7 +778,7 @@ module.exports = function(Chart) {
initToolTip: function() {
var me = this;
me.tooltip = new Chart.Tooltip({
me.tooltip = new Tooltip({
_chart: me,
_chartInstance: me, // deprecated, backward compatibility
_data: me.data,

View File

@ -96,18 +96,88 @@ defaults._set('global', {
}
});
module.exports = function(Chart) {
var positioners = {
/**
* Helper method to merge the opacity into a color
* Average mode places the tooltip at the average position of the elements shown
* @function Chart.Tooltip.positioners.average
* @param elements {ChartElement[]} the elements being displayed in the tooltip
* @returns {Point} tooltip position
*/
function mergeOpacity(colorString, opacity) {
var color = helpers.color(colorString);
return color.alpha(opacity * color.alpha()).rgbaString();
average: function(elements) {
if (!elements.length) {
return false;
}
// Helper to push or concat based on if the 2nd parameter is an array or not
function pushOrConcat(base, toPush) {
var i, len;
var x = 0;
var y = 0;
var count = 0;
for (i = 0, len = elements.length; i < len; ++i) {
var el = elements[i];
if (el && el.hasValue()) {
var pos = el.tooltipPosition();
x += pos.x;
y += pos.y;
++count;
}
}
return {
x: Math.round(x / count),
y: Math.round(y / count)
};
},
/**
* Gets the tooltip position nearest of the item nearest to the event position
* @function Chart.Tooltip.positioners.nearest
* @param elements {Chart.Element[]} the tooltip elements
* @param eventPosition {Point} the position of the event in canvas coordinates
* @returns {Point} the tooltip position
*/
nearest: function(elements, eventPosition) {
var x = eventPosition.x;
var y = eventPosition.y;
var minDistance = Number.POSITIVE_INFINITY;
var i, len, nearestElement;
for (i = 0, len = elements.length; i < len; ++i) {
var el = elements[i];
if (el && el.hasValue()) {
var center = el.getCenterPoint();
var d = helpers.distanceBetweenPoints(eventPosition, center);
if (d < minDistance) {
minDistance = d;
nearestElement = el;
}
}
}
if (nearestElement) {
var tp = nearestElement.tooltipPosition();
x = tp.x;
y = tp.y;
}
return {
x: x,
y: y
};
}
};
/**
* Helper method to merge the opacity into a color
*/
function mergeOpacity(colorString, opacity) {
var color = helpers.color(colorString);
return color.alpha(opacity * color.alpha()).rgbaString();
}
// Helper to push or concat based on if the 2nd parameter is an array or not
function pushOrConcat(base, toPush) {
if (toPush) {
if (helpers.isArray(toPush)) {
// base = base.concat(toPush);
@ -118,12 +188,12 @@ module.exports = function(Chart) {
}
return base;
}
}
// Private helper to create a tooltip item model
// @param element : the chart element (point, arc, bar) to create the tooltip item for
// @return : new tooltip item
function createTooltipItem(element) {
// Private helper to create a tooltip item model
// @param element : the chart element (point, arc, bar) to create the tooltip item for
// @return : new tooltip item
function createTooltipItem(element) {
var xScale = element._xScale;
var yScale = element._yScale || element._scale; // handle radar || polarArea charts
var index = element._index;
@ -137,13 +207,13 @@ module.exports = function(Chart) {
x: element._model.x,
y: element._model.y
};
}
}
/**
/**
* Helper to get the reset model for the tooltip
* @param tooltipOpts {Object} the tooltip options
*/
function getBaseModel(tooltipOpts) {
function getBaseModel(tooltipOpts) {
var globalDefaults = defaults.global;
var valueOrDefault = helpers.valueOrDefault;
@ -190,12 +260,12 @@ module.exports = function(Chart) {
borderColor: tooltipOpts.borderColor,
borderWidth: tooltipOpts.borderWidth
};
}
}
/**
/**
* Get the size of the tooltip
*/
function getTooltipSize(tooltip, model) {
function getTooltipSize(tooltip, model) {
var ctx = tooltip._chart.ctx;
var height = model.yPadding * 2; // Tooltip Padding
@ -258,12 +328,12 @@ module.exports = function(Chart) {
width: width,
height: height
};
}
}
/**
/**
* Helper to get the alignment of a tooltip given the size
*/
function determineAlignment(tooltip, size) {
function determineAlignment(tooltip, size) {
var model = tooltip._model;
var chart = tooltip._chart;
var chartArea = tooltip._chart.chartArea;
@ -331,12 +401,12 @@ module.exports = function(Chart) {
xAlign: opts.xAlign ? opts.xAlign : xAlign,
yAlign: opts.yAlign ? opts.yAlign : yAlign
};
}
}
/**
/**
* @Helper to get the location a tooltip needs to be placed at given the initial position (via the vm) and the size and alignment
*/
function getBackgroundPoint(vm, size, alignment, chart) {
function getBackgroundPoint(vm, size, alignment, chart) {
// Background Position
var x = vm.x;
var y = vm.y;
@ -385,9 +455,9 @@ module.exports = function(Chart) {
x: x,
y: y
};
}
}
Chart.Tooltip = Element.extend({
var exports = module.exports = Element.extend({
initialize: function() {
this._model = getBaseModel(this._options);
this._lastActive = [];
@ -502,7 +572,7 @@ module.exports = function(Chart) {
var labelColors = [];
var labelTextColors = [];
tooltipPosition = Chart.Tooltip.positioners[opts.position].call(me, active, me._eventPosition);
tooltipPosition = positioners[opts.position].call(me, active, me._eventPosition);
var tooltipItems = [];
for (i = 0, len = active.length; i < len; ++i) {
@ -575,6 +645,7 @@ module.exports = function(Chart) {
return me;
},
drawCaret: function(tooltipPoint, size) {
var ctx = this._chart.ctx;
var vm = this._view;
@ -643,6 +714,7 @@ module.exports = function(Chart) {
}
return {x1: x1, x2: x2, x3: x3, y1: y1, y2: y2, y3: y3};
},
drawTitle: function(pt, vm, ctx, opacity) {
var title = vm.title;
@ -667,6 +739,7 @@ module.exports = function(Chart) {
}
}
},
drawBody: function(pt, vm, ctx, opacity) {
var bodyFontSize = vm.bodyFontSize;
var bodySpacing = vm.bodySpacing;
@ -727,6 +800,7 @@ module.exports = function(Chart) {
helpers.each(vm.afterBody, fillLineOfText);
pt.y -= bodySpacing; // Remove last body spacing
},
drawFooter: function(pt, vm, ctx, opacity) {
var footer = vm.footer;
@ -745,6 +819,7 @@ module.exports = function(Chart) {
});
}
},
drawBackground: function(pt, vm, ctx, tooltipSize, opacity) {
ctx.fillStyle = mergeOpacity(vm.backgroundColor, opacity);
ctx.strokeStyle = mergeOpacity(vm.borderColor, opacity);
@ -787,6 +862,7 @@ module.exports = function(Chart) {
ctx.stroke();
}
},
draw: function() {
var ctx = this._chart.ctx;
var vm = this._view;
@ -869,80 +945,10 @@ module.exports = function(Chart) {
return changed;
}
});
});
/**
/**
* @namespace Chart.Tooltip.positioners
*/
Chart.Tooltip.positioners = {
/**
* Average mode places the tooltip at the average position of the elements shown
* @function Chart.Tooltip.positioners.average
* @param elements {ChartElement[]} the elements being displayed in the tooltip
* @returns {Point} tooltip position
*/
average: function(elements) {
if (!elements.length) {
return false;
}
exports.positioners = positioners;
var i, len;
var x = 0;
var y = 0;
var count = 0;
for (i = 0, len = elements.length; i < len; ++i) {
var el = elements[i];
if (el && el.hasValue()) {
var pos = el.tooltipPosition();
x += pos.x;
y += pos.y;
++count;
}
}
return {
x: Math.round(x / count),
y: Math.round(y / count)
};
},
/**
* Gets the tooltip position nearest of the item nearest to the event position
* @function Chart.Tooltip.positioners.nearest
* @param elements {Chart.Element[]} the tooltip elements
* @param eventPosition {Point} the position of the event in canvas coordinates
* @returns {Point} the tooltip position
*/
nearest: function(elements, eventPosition) {
var x = eventPosition.x;
var y = eventPosition.y;
var minDistance = Number.POSITIVE_INFINITY;
var i, len, nearestElement;
for (i = 0, len = elements.length; i < len; ++i) {
var el = elements[i];
if (el && el.hasValue()) {
var center = el.getCenterPoint();
var d = helpers.distanceBetweenPoints(eventPosition, center);
if (d < minDistance) {
minDistance = d;
nearestElement = el;
}
}
}
if (nearestElement) {
var tp = nearestElement.tooltipPosition();
x = tp.x;
y = tp.y;
}
return {
x: x,
y: y
};
}
};
};

View File

@ -1,4 +1,3 @@
// Test the bubble chart default config
describe('Default Configs', function() {
describe('Bubble Chart', function() {
it('should return correct tooltip strings', function() {

View File

@ -0,0 +1,44 @@
describe('Chart namespace', function() {
describe('Chart', function() {
it('should a function (constructor)', function() {
expect(Chart instanceof Function).toBeTruthy();
});
it('should define "core" properties', function() {
expect(Chart instanceof Function).toBeTruthy();
expect(Chart.Animation instanceof Object).toBeTruthy();
expect(Chart.animationService instanceof Object).toBeTruthy();
expect(Chart.defaults instanceof Object).toBeTruthy();
expect(Chart.Element instanceof Object).toBeTruthy();
expect(Chart.Interaction instanceof Object).toBeTruthy();
expect(Chart.layouts instanceof Object).toBeTruthy();
expect(Chart.plugins instanceof Object).toBeTruthy();
expect(Chart.platform instanceof Object).toBeTruthy();
expect(Chart.Ticks instanceof Object).toBeTruthy();
expect(Chart.Tooltip instanceof Object).toBeTruthy();
expect(Chart.Tooltip.positioners instanceof Object).toBeTruthy();
});
});
describe('Chart.elements', function() {
it('should be an object', function() {
expect(Chart.elements instanceof Object).toBeTruthy();
});
it('should contains "elements" classes', function() {
expect(Chart.elements.Arc instanceof Function).toBeTruthy();
expect(Chart.elements.Line instanceof Function).toBeTruthy();
expect(Chart.elements.Point instanceof Function).toBeTruthy();
expect(Chart.elements.Rectangle instanceof Function).toBeTruthy();
});
});
describe('Chart.helpers', function() {
it('should be an object', function() {
expect(Chart.helpers instanceof Object).toBeTruthy();
});
it('should contains "helpers" namespaces', function() {
expect(Chart.helpers.easing instanceof Object).toBeTruthy();
expect(Chart.helpers.canvas instanceof Object).toBeTruthy();
expect(Chart.helpers.options instanceof Object).toBeTruthy();
});
});
});