diff --git a/src/core/core.animations.js b/src/core/core.animations.js index 4ee61b84b0d..00ca73d4b61 100644 --- a/src/core/core.animations.js +++ b/src/core/core.animations.js @@ -1,12 +1,13 @@ import animator from './core.animator.js'; import Animation from './core.animation.js'; import defaults from './core.defaults.js'; -import {isArray, isObject} from '../helpers/helpers.core.js'; +import {isArray, isObject, _splitKey} from '../helpers/helpers.core.js'; export default class Animations { constructor(chart, config) { this._chart = chart; this._properties = new Map(); + this._pathProperties = new Map(); this.configure(config); } @@ -34,6 +35,9 @@ export default class Animations { } }); }); + + const pathAnimatedProps = this._pathProperties; + loadPathOptions(animatedProps, pathAnimatedProps); } /** @@ -67,23 +71,15 @@ export default class Animations { */ _createAnimations(target, values) { const animatedProps = this._properties; + const pathAnimatedProps = this._pathProperties; const animations = []; const running = target.$animations || (target.$animations = {}); const props = Object.keys(values); const date = Date.now(); - let i; - for (i = props.length - 1; i >= 0; --i) { - const prop = props[i]; - if (prop.charAt(0) === '$') { - continue; - } - - if (prop === 'options') { - animations.push(...this._animateOptions(target, values)); - continue; - } - const value = values[prop]; + const manageItem = function(tgt, vals, prop, subProp) { + const key = subProp || prop; + const value = vals[key]; let animation = running[prop]; const cfg = animatedProps.get(prop); @@ -91,30 +87,52 @@ export default class Animations { if (cfg && animation.active()) { // There is an existing active animation, let's update that animation.update(cfg, value, date); - continue; - } else { - animation.cancel(); + return; } + animation.cancel(); } if (!cfg || !cfg.duration) { // not animated, set directly to new value - target[prop] = value; - continue; + tgt[key] = value; + return; } - - running[prop] = animation = new Animation(cfg, target, prop, value); + running[prop] = animation = new Animation(cfg, tgt, key, value); animations.push(animation); + }; + + let i; + for (i = props.length - 1; i >= 0; --i) { + const prop = props[i]; + if (prop.charAt(0) === '$') { + continue; + } + if (prop === 'options') { + animations.push(...this._animateOptions(target, values)); + continue; + } + const propValue = pathAnimatedProps.get(prop); + if (propValue) { + propValue.forEach(function(item) { + const newTarget = getInnerObject(target, item); + const newValues = newTarget && getInnerObject(values, item); + if (newValues) { + manageItem(newTarget, newValues, item.prop, item.key); + } + }); + } else { + manageItem(target, values, prop); + } } return animations; } /** - * Update `target` properties to new values, using configured animations - * @param {object} target - object to update - * @param {object} values - new target properties - * @returns {boolean|undefined} - `true` if animations were started - **/ + * Update `target` properties to new values, using configured animations + * @param {object} target - object to update + * @param {object} values - new target properties + * @returns {boolean|undefined} - `true` if animations were started + **/ update(target, values) { if (this._properties.size === 0) { // Nothing is animated, just apply the new values. @@ -131,6 +149,18 @@ export default class Animations { } } +function loadPathOptions(props, pathProps) { + props.forEach(function(v, k) { + const value = parserPathOptions(k); + if (value) { + const mapKey = value.path[0]; + const mapValue = pathProps.get(mapKey) || []; + mapValue.push(value); + pathProps.set(mapKey, mapValue); + } + }); +} + function awaitAll(animations, properties) { const running = []; const keys = Object.keys(properties); @@ -160,3 +190,39 @@ function resolveTargetOptions(target, newOptions) { } return options; } + +function parserPathOptions(key) { + if (key.includes('.')) { + return parseKeys(key, _splitKey(key)); + } +} + +function parseKeys(key, keys) { + const result = { + prop: key, + path: [] + }; + for (let i = 0, n = keys.length; i < n; i++) { + const k = keys[i]; + if (!k.trim().length) { // empty string + return; + } + if (i === (n - 1)) { + result.key = k; + } else { + result.path.push(k); + } + } + return result; +} + +function getInnerObject(target, pathOpts) { + let obj = target; + for (const p of pathOpts.path) { + obj = obj[p]; + if (!isObject(obj)) { + return; + } + } + return obj; +} diff --git a/test/specs/core.animations.tests.js b/test/specs/core.animations.tests.js index 6fddb20445f..bdd319694bc 100644 --- a/test/specs/core.animations.tests.js +++ b/test/specs/core.animations.tests.js @@ -93,6 +93,216 @@ describe('Chart.animations', function() { }, 300); }); + it('should update path properties to target during animation', function(done) { + const chart = { + draw: function() {}, + options: { + } + }; + const anims = new Chart.Animations(chart, {value: {properties: ['level.value'], type: 'number', duration: 500}}); + + const from = 0; + const to = 100; + const target = { + level: { + value: from + } + }; + expect(anims.update(target, { + level: { + value: to + } + })).toBeTrue(); + + const ended = function() { + const value = target.level.value; + expect(value === to).toBeTrue(); + Chart.animator.remove(chart); + done(); + }; + + Chart.animator.listen(chart, 'complete', ended); + Chart.animator.start(chart); + setTimeout(function() { + const value = target.level.value; + expect(value > from).toBeTrue(); + expect(value < to).toBeTrue(); + }, 250); + }); + + it('should update multiple path properties with the same root to target during animation', function(done) { + const chart = { + draw: function() {}, + options: { + } + }; + const anims = new Chart.Animations(chart, {value: {properties: ['level.value1', 'level.value2'], type: 'number', duration: 500}}); + + const from = 0; + const to = 100; + const target = { + level: { + value1: from, + value2: from + } + }; + expect(anims.update(target, { + level: { + value1: to, + value2: to + } + })).toBeTrue(); + + const ended = function() { + const value1 = target.level.value1; + expect(value1 === to).toBeTrue(); + const value2 = target.level.value2; + expect(value2 === to).toBeTrue(); + Chart.animator.remove(chart); + done(); + }; + + Chart.animator.listen(chart, 'complete', ended); + Chart.animator.start(chart); + setTimeout(function() { + const value1 = target.level.value1; + const value2 = target.level.value2; + expect(value1 > from).toBeTrue(); + expect(value1 < to).toBeTrue(); + expect(value2 > from).toBeTrue(); + expect(value2 < to).toBeTrue(); + }, 250); + }); + + it('should not update path properties to target during animation because not an object', function() { + const chart = { + draw: function() {}, + options: { + } + }; + const anims = new Chart.Animations(chart, {value: {properties: ['level.value'], type: 'number'}}); + + const from = 0; + const to = 100; + const target = { + level: from + }; + expect(anims.update(target, { + level: to + })).toBeUndefined(); + }); + + it('should not update path properties to target during animation because missing target', function() { + const chart = { + draw: function() {}, + options: { + } + }; + const anims = new Chart.Animations(chart, {value: {properties: ['level.value'], type: 'number'}}); + + const from = 0; + const to = 100; + const target = { + foo: from + }; + + expect(anims.update(target, { + foo: to + })).toBeUndefined(); + }); + + it('should not update path properties to target during animation because properties not consistent', function() { + const chart = { + draw: function() {}, + options: { + } + }; + const anims = new Chart.Animations(chart, {value: {properties: ['.value', 'value.', 'value..end'], type: 'number'}}); + expect(anims._pathProperties.size === 0).toBeTrue(); + }); + + it('should update path (2 levels) properties to target during animation', function(done) { + const chart = { + draw: function() {}, + options: { + } + }; + const anims = new Chart.Animations(chart, {value: {properties: ['level1.level2.value'], type: 'number', duration: 500}}); + + const from = 0; + const to = 100; + const target = { + level1: { + level2: { + value: from + } + } + }; + expect(anims.update(target, { + level1: { + level2: { + value: to + } + } + })).toBeTrue(); + + const ended = function() { + const value = target.level1.level2.value; + expect(value === to).toBeTrue(); + Chart.animator.remove(chart); + done(); + }; + + Chart.animator.listen(chart, 'complete', ended); + Chart.animator.start(chart); + setTimeout(function() { + const value = target.level1.level2.value; + expect(value > from).toBeTrue(); + expect(value < to).toBeTrue(); + }, 250); + }); + + it('should update path properties to target options during animation', function(done) { + const chart = { + draw: function() {}, + options: { + } + }; + const anims = new Chart.Animations(chart, {value: {properties: ['level.value'], type: 'number', duration: 500}}); + + const from = 0; + const to = 100; + const target = { + options: { + level: { + value: from + } + } + }; + expect(anims.update(target, { + options: { + level: { + value: to + } + } + })).toBeTrue(); + + const ended = function() { + const value = target.options.level.value; + expect(value === to).toBeTrue(); + Chart.animator.remove(chart); + done(); + }; + + Chart.animator.listen(chart, 'complete', ended); + Chart.animator.start(chart); + setTimeout(function() { + const value = target.options.level.value; + expect(value > from).toBeTrue(); + expect(value < to).toBeTrue(); + }, 250); + }); + it('should not assign shared options to target when animations are cancelled', function(done) { const chart = { draw: function() {},
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: