583 lines
12 KiB
JavaScript
583 lines
12 KiB
JavaScript
/*
|
|
Plugin Name: amCharts Animate
|
|
Description: Smoothly animates the `dataProvider`
|
|
Author: Paul Chapman, amCharts
|
|
Version: 1.1.2
|
|
Author URI: http://www.amcharts.com/
|
|
|
|
Copyright 2015 amCharts
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
|
|
Please note that the above license covers only this plugin. It by all means does
|
|
not apply to any other amCharts products that are covered by different licenses.
|
|
*/
|
|
|
|
/* globals AmCharts */
|
|
/* jshint -W061 */
|
|
|
|
( function() {
|
|
"use strict";
|
|
|
|
|
|
// For older browsers, e.g. IE9 and lower
|
|
if ( typeof requestAnimationFrame === "undefined" ) {
|
|
var fps = 1000 / 60;
|
|
|
|
var raf = function( f ) {
|
|
setTimeout( function() {
|
|
f( new Date().getTime() );
|
|
}, fps );
|
|
};
|
|
|
|
} else {
|
|
var raf = requestAnimationFrame;
|
|
}
|
|
|
|
|
|
function tween( time, from, to ) {
|
|
return ( time * ( to - from ) ) + from;
|
|
}
|
|
|
|
|
|
function easeInOut3( t ) {
|
|
var r = ( t < 0.5 ? t * 2 : ( 1 - t ) * 2 );
|
|
r *= r * r * r;
|
|
return ( t < 0.5 ? r / 2 : 1 - ( r / 2 ) );
|
|
}
|
|
|
|
function easeIn3( t ) {
|
|
t *= t * t * t;
|
|
return t;
|
|
}
|
|
|
|
function easeOut3( t ) {
|
|
var r = ( 1 - t );
|
|
r *= r * r * r;
|
|
return ( 1 - r );
|
|
}
|
|
|
|
|
|
function Tween( object, key, from, to ) {
|
|
this._object = object;
|
|
this._key = key;
|
|
this._from = from;
|
|
this._to = to;
|
|
}
|
|
|
|
Tween.prototype.interpolate = function( time ) {
|
|
this._object[ this._key ] = tween( time, this._from, this._to );
|
|
};
|
|
|
|
|
|
function Animation( duration, easing, onComplete, tweens, chart ) {
|
|
this._finished = false;
|
|
this._startTime = null;
|
|
|
|
this._duration = duration;
|
|
this._easing = ( easing == null ? easeOut3 : easing );
|
|
this._onComplete = onComplete;
|
|
this._tweens = tweens;
|
|
|
|
this._chart = chart;
|
|
}
|
|
|
|
Animation.prototype.cancel = function() {
|
|
this._finished = true;
|
|
this._startTime = null;
|
|
|
|
this._duration = null;
|
|
this._easing = null;
|
|
this._onComplete = null;
|
|
this._tweens = null;
|
|
|
|
this._chart = null;
|
|
};
|
|
|
|
Animation.prototype._onFrame = function( now ) {
|
|
// This will only happen when the animation was cancelled
|
|
if ( this._finished ) {
|
|
return true;
|
|
|
|
} else if ( this._startTime === null ) {
|
|
this._startTime = now;
|
|
return false;
|
|
|
|
} else {
|
|
var diff = now - this._startTime;
|
|
|
|
if ( diff < this._duration ) {
|
|
this._tick( diff / this._duration );
|
|
return false;
|
|
|
|
} else {
|
|
this._end( 1 );
|
|
// Cleanup all the properties
|
|
this.cancel();
|
|
return true;
|
|
}
|
|
}
|
|
};
|
|
|
|
Animation.prototype._tick = function( time ) {
|
|
// Apply the easing to the time ratio
|
|
time = this._easing( time );
|
|
|
|
var tweens = this._tweens;
|
|
|
|
for ( var i = 0; i < tweens.length; ++i ) {
|
|
tweens[ i ].interpolate( time );
|
|
}
|
|
|
|
// TODO check the performance of this
|
|
pushNew( needsValidation, this._chart );
|
|
};
|
|
|
|
Animation.prototype._end = function( time ) {
|
|
this._tick( time );
|
|
|
|
this._onComplete();
|
|
};
|
|
|
|
|
|
function Animator() {
|
|
this._animating = false;
|
|
this._animations = [];
|
|
this._onBeforeFrames = [];
|
|
this._onAfterFrames = [];
|
|
|
|
var self = this;
|
|
|
|
this._raf = function( now ) {
|
|
self._onFrame( now );
|
|
};
|
|
}
|
|
|
|
Animator.prototype.animate = function( animation ) {
|
|
this._animations.push( animation );
|
|
|
|
if ( !this._animating ) {
|
|
this._animating = true;
|
|
|
|
raf( this._raf );
|
|
}
|
|
};
|
|
|
|
|
|
Animator.prototype.onBeforeFrame = function( f ) {
|
|
this._onBeforeFrames.push( f );
|
|
};
|
|
|
|
Animator.prototype.onAfterFrame = function( f ) {
|
|
this._onAfterFrames.push( f );
|
|
};
|
|
|
|
|
|
Animator.prototype._onFrame = function( now ) {
|
|
var onBeforeFrames = this._onBeforeFrames;
|
|
|
|
for ( var i = 0; i < onBeforeFrames.length; ++i ) {
|
|
onBeforeFrames[ i ]( now );
|
|
}
|
|
|
|
|
|
var animations = this._animations;
|
|
|
|
for ( var i = 0; i < animations.length; ++i ) {
|
|
var animation = animations[ i ];
|
|
|
|
// If the animation is finished...
|
|
if ( animation._onFrame( now ) ) {
|
|
// TODO this is a bit slow, but I don't know of a faster alternative
|
|
animations.splice( i, 1 );
|
|
--i;
|
|
}
|
|
}
|
|
|
|
|
|
var onAfterFrames = this._onAfterFrames;
|
|
|
|
for ( var i = 0; i < onAfterFrames.length; ++i ) {
|
|
onAfterFrames[ i ]( now );
|
|
}
|
|
|
|
|
|
// All animations are finished
|
|
if ( animations.length === 0 ) {
|
|
this._animating = false;
|
|
|
|
} else {
|
|
raf( this._raf );
|
|
}
|
|
};
|
|
|
|
|
|
var _animator = new Animator();
|
|
|
|
|
|
var needsValidation = [];
|
|
|
|
// This is more robust than the built-in `isNaN` function
|
|
function isNaN( x ) {
|
|
return x !== x;
|
|
}
|
|
|
|
function each( array, fn ) {
|
|
for ( var i = 0; i < array.length; ++i ) {
|
|
fn( array[ i ] );
|
|
}
|
|
}
|
|
|
|
function pushNew( array, x ) {
|
|
for ( var i = 0; i < array.length; ++i ) {
|
|
if ( array[ i ] === x ) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
array.push( x );
|
|
}
|
|
|
|
// TODO check the performance of this
|
|
_animator.onAfterFrame( function() {
|
|
for ( var i = 0; i < needsValidation.length; ++i ) {
|
|
needsValidation[ i ].validateData();
|
|
}
|
|
|
|
needsValidation.length = 0;
|
|
} );
|
|
|
|
|
|
// This ensures that a key is only added once
|
|
function addKey( keys, seen, key ) {
|
|
if ( !seen[ key ] ) {
|
|
seen[ key ] = true;
|
|
keys.push( key );
|
|
}
|
|
}
|
|
|
|
function addKeys( keys, seen, object, a ) {
|
|
each( a, function( key ) {
|
|
var value = object[ key ];
|
|
|
|
if ( value != null ) {
|
|
addKey( keys, seen, value );
|
|
}
|
|
} );
|
|
}
|
|
|
|
|
|
function getKeysSliced( chart, keys, seen ) {
|
|
addKeys( keys, seen, chart, [
|
|
"alphaField",
|
|
"valueField"
|
|
] );
|
|
}
|
|
|
|
function getKeysFunnel( chart, keys, seen ) {
|
|
getKeysSliced( chart, keys, seen );
|
|
}
|
|
|
|
function getKeysPie( chart, keys, seen ) {
|
|
getKeysSliced( chart, keys, seen );
|
|
|
|
addKeys( keys, seen, chart, [
|
|
"labelRadiusField"
|
|
] );
|
|
}
|
|
|
|
function getKeysGraph( graph, keys, seen ) {
|
|
addKeys( keys, seen, graph, [
|
|
"alphaField",
|
|
"bulletSizeField",
|
|
"closeField",
|
|
"dashLengthField",
|
|
"errorField",
|
|
"highField",
|
|
"lowField",
|
|
"openField",
|
|
"valueField"
|
|
] );
|
|
}
|
|
|
|
function getKeysXY( graph, keys, seen ) {
|
|
getKeysGraph( graph, keys, seen );
|
|
|
|
addKeys( keys, seen, graph, [
|
|
"xField",
|
|
"yField"
|
|
] );
|
|
}
|
|
|
|
function getKeysGraphs( graphs, keys, seen, f ) {
|
|
each( graphs, function( graph ) {
|
|
f( graph, keys, seen );
|
|
} );
|
|
}
|
|
|
|
function getKeysCategoryAxis( categoryAxis, keys, seen ) {
|
|
addKeys( keys, seen, categoryAxis, [
|
|
"widthField"
|
|
] );
|
|
}
|
|
|
|
|
|
// Returns an array of all of the animatable keys
|
|
function getKeys( chart ) {
|
|
var keys = [];
|
|
|
|
var seen = {};
|
|
|
|
if ( chart.type === "funnel" ) {
|
|
getKeysFunnel( chart, keys, seen );
|
|
|
|
} else if ( chart.type === "pie" ) {
|
|
getKeysPie( chart, keys, seen );
|
|
|
|
} else if ( chart.type === "serial" ) {
|
|
getKeysCategoryAxis( chart.categoryAxis, keys, seen );
|
|
getKeysGraphs( chart.graphs, keys, seen, getKeysGraph );
|
|
|
|
} else if ( chart.type === "radar" ) {
|
|
getKeysGraphs( chart.graphs, keys, seen, getKeysGraph );
|
|
|
|
} else if ( chart.type === "xy" ) {
|
|
getKeysGraphs( chart.graphs, keys, seen, getKeysXY );
|
|
}
|
|
|
|
return keys;
|
|
}
|
|
|
|
|
|
// Sets the minimum/maximum of the value axes while the animation is playing
|
|
function setAxesMinMax( chart ) {
|
|
var axes = {};
|
|
|
|
if ( chart.type === "serial" || chart.type === "radar" || chart.type === "xy" ) {
|
|
each( chart.valueAxes, function( axis ) {
|
|
// TODO is it guaranteed that every value axis has an id ?
|
|
if ( axes[ axis.id ] == null ) {
|
|
// This saves the old minimum / maximum so that we can restore it after the animation is complete
|
|
axes[ axis.id ] = {
|
|
minimum: axis.minimum,
|
|
maximum: axis.maximum
|
|
};
|
|
|
|
var min = axis.minRR;
|
|
var max = axis.maxRR;
|
|
|
|
var dif = max - min;
|
|
var difE;
|
|
|
|
if ( dif === 0 ) {
|
|
difE = Math.pow( 10, Math.floor( Math.log( Math.abs( max ) ) * Math.LOG10E ) ) / 10;
|
|
|
|
} else {
|
|
difE = Math.pow( 10, Math.floor( Math.log( Math.abs( dif ) ) * Math.LOG10E ) ) / 10;
|
|
}
|
|
|
|
if ( axis.minimum == null ) {
|
|
axis.minimum = Math.floor( min / difE ) * difE - difE;
|
|
}
|
|
|
|
if ( axis.maximum == null ) {
|
|
axis.maximum = Math.ceil( max / difE ) * difE + difE;
|
|
}
|
|
}
|
|
} );
|
|
}
|
|
|
|
return axes;
|
|
}
|
|
|
|
// Resets the minimum/maximum of the value axes after the animation is finished
|
|
function resetAxesMinMax( chart, axes ) {
|
|
if ( chart.type === "serial" || chart.type === "radar" || chart.type === "xy" ) {
|
|
each( chart.valueAxes, function( axis ) {
|
|
var info = axes[ axis.id ];
|
|
|
|
if ( info != null ) {
|
|
if ( info.minimum == null ) {
|
|
delete axis.minimum;
|
|
}
|
|
|
|
if ( info.maximum == null ) {
|
|
delete axis.maximum;
|
|
}
|
|
}
|
|
} );
|
|
}
|
|
}
|
|
|
|
|
|
function getCategoryField( chart ) {
|
|
if ( chart.type === "funnel" || chart.type === "pie" ) {
|
|
return chart.titleField;
|
|
|
|
} else if ( chart.type === "serial" || chart.type === "radar" ) {
|
|
return chart.categoryField;
|
|
}
|
|
}
|
|
|
|
|
|
function getValue( object, key ) {
|
|
var value = object[ key ];
|
|
|
|
if ( value == null ) {
|
|
return null;
|
|
|
|
} else {
|
|
value = +value;
|
|
|
|
// TODO test this
|
|
// TODO what about Infinity, etc. ?
|
|
if ( isNaN( value ) ) {
|
|
return null;
|
|
|
|
} else {
|
|
return value;
|
|
}
|
|
}
|
|
}
|
|
|
|
function getCategory( object, key ) {
|
|
var value = object[ key ];
|
|
|
|
if ( value == null ) {
|
|
return null;
|
|
|
|
} else {
|
|
// TODO better algorithm for this ?
|
|
return "" + value;
|
|
}
|
|
}
|
|
|
|
|
|
function getCategories( dataProvider, categoryField ) {
|
|
var categories = {};
|
|
|
|
each( dataProvider, function( data ) {
|
|
var category = getCategory( data, categoryField );
|
|
|
|
if ( category != null ) {
|
|
categories[ category ] = data;
|
|
}
|
|
} );
|
|
|
|
return categories;
|
|
}
|
|
|
|
|
|
function getNormalTweens( dataProvider, categoryField, categories, keys ) {
|
|
var tweens = [];
|
|
|
|
each( dataProvider, function( newData ) {
|
|
var category = getCategory( newData, categoryField );
|
|
|
|
// If the new data has the same category as the old data...
|
|
if ( category != null && category in categories ) {
|
|
var oldData = categories[ category ];
|
|
|
|
each( keys, function( key ) {
|
|
var oldValue = getValue( oldData, key );
|
|
var newValue = getValue( newData, key );
|
|
|
|
// If the old field and new field both exist...
|
|
if ( oldValue != null && newValue != null ) {
|
|
tweens.push( new Tween( newData, key, oldValue, newValue ) );
|
|
}
|
|
} );
|
|
}
|
|
} );
|
|
|
|
return tweens;
|
|
}
|
|
|
|
|
|
function getXYTweens( oldDataProvider, newDataProvider, keys ) {
|
|
var tweens = [];
|
|
|
|
var length = Math.min( oldDataProvider.length, newDataProvider.length );
|
|
|
|
for ( var i = 0; i < length; ++i ) {
|
|
var oldData = oldDataProvider[ i ];
|
|
var newData = newDataProvider[ i ];
|
|
|
|
each( keys, function( key ) {
|
|
var oldValue = getValue( oldData, key );
|
|
var newValue = getValue( newData, key );
|
|
|
|
// If the old field and new field both exist...
|
|
if ( oldValue != null && newValue != null ) {
|
|
tweens.push( new Tween( newData, key, oldValue, newValue ) );
|
|
}
|
|
} );
|
|
}
|
|
|
|
return tweens;
|
|
}
|
|
|
|
|
|
function getTweens( chart, dataProvider ) {
|
|
if ( chart.type === "xy" ) {
|
|
var keys = getKeys( chart );
|
|
|
|
return getXYTweens( chart.dataProvider, dataProvider, keys );
|
|
|
|
} else {
|
|
var categoryField = getCategoryField( chart );
|
|
var keys = getKeys( chart );
|
|
|
|
var categories = getCategories( chart.dataProvider, categoryField );
|
|
|
|
return getNormalTweens( dataProvider, categoryField, categories, keys );
|
|
}
|
|
}
|
|
|
|
|
|
function animateData( dataProvider, options ) {
|
|
var chart = this;
|
|
|
|
var tweens = getTweens( chart, dataProvider );
|
|
|
|
var axes = setAxesMinMax( chart );
|
|
|
|
chart.dataProvider = dataProvider;
|
|
|
|
function onComplete() {
|
|
resetAxesMinMax( chart, axes );
|
|
|
|
if ( options.complete != null ) {
|
|
options.complete();
|
|
}
|
|
}
|
|
|
|
var animation = new Animation(
|
|
options.duration,
|
|
options.easing,
|
|
onComplete,
|
|
tweens,
|
|
chart
|
|
);
|
|
|
|
_animator.animate( animation );
|
|
|
|
return animation;
|
|
}
|
|
|
|
|
|
AmCharts.addInitHandler( function( chart ) {
|
|
chart.animateData = animateData;
|
|
}, [ "funnel", "pie", "serial", "radar", "xy" ] );
|
|
|
|
} )();
|