}]
},
options: {
- animation: {
- tension: {
- duration: 1000,
- easing: 'linear',
- from: 1,
- to: 0,
- loop: true
- }
- },
- scales: {
- y: { // defining min and max so hiding the dataset does not change scale range
- min: 0,
- max: 100
- }
+ animations: {
+ tension: {
+ duration: 1000,
+ easing: 'linear',
+ from: 1,
+ to: 0,
+ loop: true
+ }
+ },
+ scales: {
+ y: { // defining min and max so hiding the dataset does not change scale range
+ min: 0,
+ max: 100
}
+ }
}
};
const chart = new Chart(ctx, cfg);
}]
},
options: {
- animation: {
- show: {
- x: {
- from: 0
- },
- y: {
- from: 0
- }
+ transitions: {
+ show: {
+ x: {
+ from: 0
},
- hide: {
- x: {
- to: 0
- },
- y: {
- to: 0
- }
+ y: {
+ from: 0
+ }
+ },
+ hide: {
+ x: {
+ to: 0
+ },
+ y: {
+ to: 0
}
}
+ }
}
};
const chart = new Chart(ctx, cfg);
</TabItem>
</Tabs>
-## Animation Configuration
+## Animation configuration
+
+Animation configuration consists of 3 keys.
+
+| Name | Type | Details
+| ---- | ---- | -------
+| animation | `object` | [animation](#animation)
+| animations | `object` | [animations](#animations)
+| transitions | `object` | [transitions](#transitions)
+
+These keys can be configured in following paths:
-The default configuration is defined here: <a href="https://github.com/chartjs/Chart.js/blob/master/src/core/core.animations.js#L6-L55" target="_blank">core.animations.js</a>
-Namespace: `options.animation`, the global options are defined in `Chart.defaults.animation`.
+* `` - chart options
+* `controllers[type]` - controller type options
+* `controllers[type].datasets` - dataset type options
+* `datasets[type]` - dataset type options
+
+These paths are valid under `defaults` for global confuguration and `options` for instance configuration.
+
+## animation
+
+The default configuration is defined here: <a href="https://github.com/chartjs/Chart.js/blob/master/src/core/core.animations.js#L9-L56" target="_blank">core.animations.js</a>
+
+Namespace: `options.animation`
| Name | Type | Default | Description
| ---- | ---- | ------- | -----------
| `debug` | `boolean` | `undefined` | Running animation count + FPS display in upper left corner of the chart.
| `delay` | `number` | `undefined` | Delay before starting the animations.
| `loop` | `boolean` | `undefined` | If set to `true`, the animations loop endlessly.
-| [[mode]](#animation-mode-configuration) | `object` | [defaults...](#default-modes) | Option overrides for update mode. Core modes: `'active'`, `'hide'`, `'reset'`, `'resize'`, `'show'`. See **Hide and show [mode]** example above.
-| [[property]](#animation-property-configuration) | `object` | `undefined` | Option overrides for a single element `[property]`. These have precedence over `[collection]`. See **Looping tension [property]** example above.
-| [[collection]](#animation-properties-collection-configuration) | `object` | [defaults...](#default-collections) | Option overrides for multiple properties, identified by `properties` array.
These defaults can be overridden in `options.animation` or `dataset.animation` and `tooltip.animation`. These keys are also [Scriptable](../general/options.md#scriptable-options).
-## Animation mode configuration
+## animations
-Mode option configures how an update mode animates the chart.
-The cores modes are `'active'`, `'hide'`, `'reset'`, `'resize'`, `'show'`.
-A custom mode can be used by passing a custom `mode` to [update](../developers/api.md#updatemode).
-A mode option is defined by the same options of the main [animation configuration](#animation-configuration).
+Animations options configures which element properties are animated and how.
+In addition to the main [animation configuration](#animation-configuration), the following options are available:
-### Default modes
-
-Namespace: `options.animation`
-
-| Mode | Option | Value | Description
-| -----| ------ | ----- | -----
-| `active` | duration | 400 | Override default duration to 400ms for hover animations
-| `resize` | duration | 0 | Override default duration to 0ms (= no animation) for resize
-| `show` | colors | `{ type: 'color', properties: ['borderColor', 'backgroundColor'], from: 'transparent' }` | Colors are faded in from transparent when dataset is shown using legend / [api](../developers/api.md#showdatasetIndex).
-| `show` | visible | `{ type: 'boolean', duration: 0 }` | Dataset visiblity is immediately changed to true so the color transition from transparent is visible.
-| `hide` | colors | `{ type: 'color', properties: ['borderColor', 'backgroundColor'], to: 'transparent' }` | Colors are faded to transparent when dataset id hidden using legend / [api](../developers/api.md#hidedatasetIndex).
-| `hide` | visible | `{ type: 'boolean', easing: 'easeInExpo' }` | Visibility is changed to false at a very late phase of animation
-
-## Animation property configuration
-
-Property option configures which element property to use to animate the chart and its starting and ending values.
-A property option is defined by the same options of the main [animation configuration](#animation-configuration), adding the following ones:
-
-Namespace: `options.animation[animation]`
+Namespace: `options.animations[animation]`
| Name | Type | Default | Description
| ---- | ---- | ------- | -----------
+| `properties` | `string[]` | `key` | The property names this configuration applies to. Defaults to the key name of this object.
| `type` | `string` | `typeof property` | Type of property, determines the interpolator used. Possible values: `'number'`, `'color'` and `'boolean'`. Only really needed for `'color'`, because `typeof` does not get that right.
| `from` | `number`\|`Color`\|`boolean` | `undefined` | Start value for the animation. Current value is used when `undefined`
| `to` | `number`\|`Color`\|`boolean` | `undefined` | End value for the animation. Updated value is used when `undefined`
| `fn` | <code><T>(from: T, to: T, factor: number) => T;</code> | `undefined` | Optional custom interpolator, instead of using a predefined interpolator from `type` |
-## Animation properties collection configuration
-
-Properties collection option configures which set of element properties to use to animate the chart.
-Collection can be named whatever you like, but should not collide with a `[property]` or `[mode]`.
-A properties collection option is defined by the same options as the [animation property configuration](#animation-property-configuration), adding the following one:
-
-The animation properties collection configuration can be adjusted in the `options.animation[collection]` namespace.
-
-| Name | Type | Default | Description
-| ---- | ---- | ------- | -----------
-| `properties` | `string[]` | `undefined` | Set of properties to use to animate the chart.
-
-### Default collections
+### Default animations
| Name | Option | Value
| ---- | ------ | -----
-| `numbers` | `type` | `'number'`
| `numbers` | `properties` | `['x', 'y', 'borderWidth', 'radius', 'tension']`
+| `numbers` | `type` | `'number'`
+| `colors` | `properties` | `['color', 'borderColor', 'backgroundColor']`
| `colors` | `type` | `'color'`
-| `colors` | `properties` | `['borderColor', 'backgroundColor']`
-
-Direct property configuration overrides configuration of same property in a collection.
-
-From collections, a property gets its configuration from first one that has its name in properties.
:::note
-These default collections are overridden by most dataset controllers.
+These default animations are overridden by most of the dataset controllers.
:::
+## transitions
+
+The core transitions are `'active'`, `'hide'`, `'reset'`, `'resize'`, `'show'`.
+A custom transtion can be used by passing a custom `mode` to [update](../developers/api.md#updatemode).
+Transition extends the main [animation configuration](#animation-configuration) and [animations configuration](#animations-configuration).
+
+### Default transitions
+
+Namespace: `options.transitions[mode]`
+
+| Mode | Option | Value | Description
+| -----| ------ | ----- | -----
+| `'active'` | animation.duration | 400 | Override default duration to 400ms for hover animations
+| `'resize'` | animation.duration | 0 | Override default duration to 0ms (= no animation) for resize
+| `'show'` | animations.colors | `{ type: 'color', properties: ['borderColor', 'backgroundColor'], from: 'transparent' }` | Colors are faded in from transparent when dataset is shown using legend / [api](../developers/api.md#showdatasetIndex).
+| `'show'` | animations.visible | `{ type: 'boolean', duration: 0 }` | Dataset visiblity is immediately changed to true so the color transition from transparent is visible.
+| `'hide'` | animations.colors | `{ type: 'color', properties: ['borderColor', 'backgroundColor'], to: 'transparent' }` | Colors are faded to transparent when dataset id hidden using legend / [api](../developers/api.md#hidedatasetIndex).
+| `'hide'` | animations.visible | `{ type: 'boolean', easing: 'easeInExpo' }` | Visibility is changed to false at a very late phase of animation
+
## Disabling animation
To disable an animation configuration, the animation node must be set to `false`, with the exception for animation modes which can be disabled by setting the `duration` to `0`.
```javascript
-chart.options.animation = false; // disables the whole animation
-chart.options.animation.active.duration = 0; // disables the animation for 'active' mode
-chart.options.animation.colors = false; // disables animation defined by the collection of 'colors' properties
-chart.options.animation.x = false; // disables animation defined by the 'x' property
+chart.options.animation = false; // disables all animations
+chart.options.animations.colors = false; // disables animation defined by the collection of 'colors' properties
+chart.options.animations.x = false; // disables animation defined by the 'x' property
+chart.options.transitions.active.animation.duration = 0; // disables the animation for 'active' mode
```
## Easing
myLineChart.update(); // Calling update now animates the position of March from 90 to 50.
```
-A `mode` string can be provided to indicate what should be updated and what animation configuration should be used. Core calls this method using any of `'active'`, `'hide'`, `'reset'`, `'resize'`, `'show'` or `undefined`. `'none'` is also a supported mode for skipping animations for single update. Please see [animations](../configuration/animations.mdx) docs for more details.
+A `mode` string can be provided to indicate transition configuration should be used. Core calls this method using any of `'active'`, `'hide'`, `'reset'`, `'resize'`, `'show'` or `undefined`. `'none'` is also a supported mode for skipping animations for single update. Please see [animations](../configuration/animations.mdx) docs for more details.
Example:
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
datasets: [{
label: 'My First dataset',
- animation: {
+ animations: {
y: {
duration: 2000,
- delay: 100
+ delay: 500
}
},
- backgroundColor: window.chartColors.red,
+ backgroundColor: 'rgba(170,0,0,0.1)',
borderColor: window.chartColors.red,
data: [
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor()
],
- fill: false,
+ fill: 1,
+ tension: 0.5
}, {
label: 'My Second dataset',
fill: false,
}]
},
options: {
- animation: {
+ animations: {
y: {
easing: 'easeInOutElastic',
- from: 0
+ from: (ctx) => {
+ if (ctx.type === 'data') {
+ if (ctx.mode === 'default' && !ctx.dropped) {
+ ctx.dropped = true;
+ return 0;
+ }
+ }
+ }
}
},
responsive: true,
}]
},
options: {
- animation: {
+ animations: {
radius: {
duration: 400,
easing: 'linear',
hoverRadius: 6
}
},
- responsive: true,
+ interaction: {
+ mode: 'nearest',
+ axis: 'x',
+ intersect: false
+ },
plugins: {
title: {
display: true,
text: 'Chart.js Line Chart'
},
- tooltip: {
- mode: 'nearest',
- axis: 'x',
- intersect: false,
- },
- },
- hover: {
- mode: 'nearest',
- axis: 'x',
- intersect: false
},
+ responsive: true,
scales: {
x: {
display: true,
datasets: {
categoryPercentage: 0.8,
barPercentage: 0.9,
- animation: {
+ animations: {
numbers: {
type: 'number',
properties: ['x', 'y', 'base', 'width', 'height']
BubbleController.defaults = {
datasetElementType: false,
dataElementType: 'point',
- animation: {
+ animations: {
numbers: {
+ type: 'number',
properties: ['x', 'y', 'borderWidth', 'radius']
}
},
datasetElementType: false,
dataElementType: 'arc',
animation: {
- numbers: {
- type: 'number',
- properties: ['circumference', 'endAngle', 'innerRadius', 'outerRadius', 'startAngle', 'x', 'y', 'offset', 'borderWidth']
- },
// Boolean - Whether we animate the rotation of the Doughnut
animateRotate: true,
// Boolean - Whether we animate scaling the Doughnut from the centre
animateScale: false
},
+ animations: {
+ numbers: {
+ type: 'number',
+ properties: ['circumference', 'endAngle', 'innerRadius', 'outerRadius', 'startAngle', 'x', 'y', 'offset', 'borderWidth']
+ },
+ },
aspectRatio: 1,
datasets: {
PolarAreaController.defaults = {
dataElementType: 'arc',
animation: {
+ animateRotate: true,
+ animateScale: true
+ },
+ animations: {
numbers: {
type: 'number',
properties: ['x', 'y', 'startAngle', 'endAngle', 'innerRadius', 'outerRadius']
},
- animateRotate: true,
- animateScale: true
},
aspectRatio: 1,
indexAxis: 'r',
this._active = true;
this._fn = cfg.fn || interpolators[cfg.type || typeof from];
- this._easing = effects[cfg.easing || 'linear'];
+ this._easing = effects[cfg.easing] || effects.linear;
this._start = Math.floor(Date.now() + (cfg.delay || 0));
this._duration = Math.floor(cfg.duration);
this._loop = !!cfg.loop;
import animator from './core.animator';
import Animation from './core.animation';
import defaults from './core.defaults';
-import {isObject} from '../helpers/helpers.core';
+import {isArray, isObject} from '../helpers/helpers.core';
const numbers = ['x', 'y', 'borderWidth', 'radius', 'tension'];
-const colors = ['borderColor', 'backgroundColor'];
-const animationOptions = ['delay', 'duration', 'easing', 'fn', 'from', 'loop', 'to', 'type'];
+const colors = ['color', 'borderColor', 'backgroundColor'];
defaults.set('animation', {
- // Plain properties can be overridden in each object
+ delay: undefined,
duration: 1000,
easing: 'easeOutQuart',
- onProgress: undefined,
- onComplete: undefined,
+ fn: undefined,
+ from: undefined,
+ loop: undefined,
+ to: undefined,
+ type: undefined,
+});
+
+const animationOptions = Object.keys(defaults.animation);
- // Property sets
+defaults.describe('animation', {
+ _fallback: false,
+ _indexable: false,
+ _scriptable: (name) => name !== 'onProgress' && name !== 'onComplete' && name !== 'fn',
+});
+
+defaults.set('animations', {
colors: {
type: 'color',
properties: colors
type: 'number',
properties: numbers
},
+});
+
+defaults.describe('animations', {
+ _fallback: 'animation',
+});
- // Update modes. These are overrides / additions to the above animations.
+defaults.set('transitions', {
active: {
- duration: 400
+ animation: {
+ duration: 400
+ }
},
resize: {
- duration: 0
+ animation: {
+ duration: 0
+ }
},
show: {
- colors: {
- type: 'color',
- properties: colors,
- from: 'transparent'
- },
- visible: {
- type: 'boolean',
- duration: 0 // show immediately
- },
+ animations: {
+ colors: {
+ from: 'transparent'
+ },
+ visible: {
+ type: 'boolean',
+ duration: 0 // show immediately
+ },
+ }
},
hide: {
- colors: {
- type: 'color',
- properties: colors,
- to: 'transparent'
- },
- visible: {
- type: 'boolean',
- fn: v => v < 1 ? 0 : 1 // for keeping the dataset visible all the way through the animation
- },
+ animations: {
+ colors: {
+ to: 'transparent'
+ },
+ visible: {
+ type: 'boolean',
+ fn: v => v < 1 ? 0 : 1 // for keeping the dataset visible all the way through the animation
+ },
+ }
}
});
-defaults.describe('animation', {
- _scriptable: (name) => name !== 'onProgress' && name !== 'onComplete' && name !== 'fn',
- _indexable: false,
- _fallback: 'animation',
-});
-
export default class Animations {
- constructor(chart, animations) {
+ constructor(chart, config) {
this._chart = chart;
this._properties = new Map();
- this.configure(animations);
+ this.configure(config);
}
- configure(animations) {
- if (!isObject(animations)) {
+ configure(config) {
+ if (!isObject(config)) {
return;
}
const animatedProps = this._properties;
- Object.getOwnPropertyNames(animations).forEach(key => {
- const cfg = animations[key];
+ Object.getOwnPropertyNames(config).forEach(key => {
+ const cfg = config[key];
if (!isObject(cfg)) {
return;
}
resolved[option] = cfg[option];
}
- (cfg.properties || [key]).forEach((prop) => {
+ (isArray(cfg.properties) && cfg.properties || [key]).forEach((prop) => {
if (prop === key || !animatedProps.has(prop)) {
animatedProps.set(prop, resolved);
}
}
/**
- * Returns the option scope keys for resolving dataset options.
- * These keys do not include the dataset itself, because it is not under options.
- * @param {string} datasetType
- * @return {string[]}
- */
+ * Returns the option scope keys for resolving dataset options.
+ * These keys do not include the dataset itself, because it is not under options.
+ * @param {string} datasetType
+ * @return {string[]}
+ */
datasetScopeKeys(datasetType) {
return cachedKeys(datasetType,
() => [
}
/**
- * Returns the option scope keys for resolving dataset animation options.
- * These keys do not include the dataset itself, because it is not under options.
- * @param {string} datasetType
- * @return {string[]}
- */
- datasetAnimationScopeKeys(datasetType) {
- return cachedKeys(`${datasetType}.animation`,
+ * Returns the option scope keys for resolving dataset animation options.
+ * These keys do not include the dataset itself, because it is not under options.
+ * @param {string} datasetType
+ * @param {string} transition
+ * @return {string[]}
+ */
+ datasetAnimationScopeKeys(datasetType, transition) {
+ return cachedKeys(`${datasetType}.transition.${transition}`,
() => [
- `datasets.${datasetType}.animation`,
- `controllers.${datasetType}.animation`,
- `controllers.${datasetType}.datasets.animation`,
- 'animation'
+ `datasets.${datasetType}.transitions.${transition}`,
+ `controllers.${datasetType}.transitions.${transition}`,
+ `controllers.${datasetType}.datasets.transitions.${transition}`,
+ `transitions.${transition}`,
+ `datasets.${datasetType}`,
+ `controllers.${datasetType}`,
+ `controllers.${datasetType}.datasets`,
+ ''
]);
}
/**
- * Returns the options scope keys for resolving element options that belong
- * to an dataset. These keys do not include the dataset itself, because it
- * is not under options.
- * @param {string} datasetType
- * @param {string} elementType
- * @return {string[]}
- */
+ * Returns the options scope keys for resolving element options that belong
+ * to an dataset. These keys do not include the dataset itself, because it
+ * is not under options.
+ * @param {string} datasetType
+ * @param {string} elementType
+ * @return {string[]}
+ */
datasetElementScopeKeys(datasetType, elementType) {
return cachedKeys(`${datasetType}-${elementType}`,
() => [
/**
* Returns the options scope keys for resolving plugin options.
* @param {{id: string, additionalOptionScopes?: string[]}} plugin
- * @return {string[]}
+ * @return {string[]}
*/
pluginScopeKeys(plugin) {
const id = plugin.id;
}
/**
- * Resolves the objects from options and defaults for option value resolution.
- * @param {object} mainScope - The main scope object for options
- * @param {string[]} scopeKeys - The keys in resolution order
+ * Resolves the objects from options and defaults for option value resolution.
+ * @param {object} mainScope - The main scope object for options
+ * @param {string[]} scopeKeys - The keys in resolution order
* @param {boolean} [resetCache] - reset the cache for this mainScope
- */
+ */
getOptionScopes(mainScope, scopeKeys, resetCache) {
let cache = this._scopeCache.get(mainScope);
if (!cache || resetCache) {
}
/**
- * Returns the option scopes for resolving chart options
- * @return {object[]}
- */
+ * Returns the option scopes for resolving chart options
+ * @return {object[]}
+ */
chartOptionScopes() {
return [
this.options,
}
/**
- * @param {object[]} scopes
- * @param {string[]} names
- * @param {function|object} context
- * @param {string[]} [prefixes]
- * @return {object}
- */
+ * @param {object[]} scopes
+ * @param {string[]} names
+ * @param {function|object} context
+ * @param {string[]} [prefixes]
+ * @return {object}
+ */
resolveNamedOptions(scopes, names, context, prefixes = ['']) {
const result = {$shared: true};
const {resolver, subPrefixes} = getResolver(this._resolverCache, scopes, prefixes);
}
/**
- * @param {object[]} scopes
- * @param {object} [context]
+ * @param {object[]} scopes
+ * @param {object} [context]
* @param {string[]} [prefixes]
- */
+ */
createResolver(scopes, context, prefixes = ['']) {
const {resolver} = getResolver(this._resolverCache, scopes, prefixes);
return isObject(context)
for (const prop of names) {
if ((isScriptable(prop) && isFunction(proxy[prop]))
- || (isIndexable(prop) && isArray(proxy[prop]))) {
+ || (isIndexable(prop) && isArray(proxy[prop]))) {
return true;
}
}
/**
* @private
*/
- _resolveAnimations(index, mode, active) {
+ _resolveAnimations(index, transition, active) {
const me = this;
const chart = me.chart;
const cache = me._cachedDataOpts;
- const cacheKey = 'animation-' + mode;
+ const cacheKey = `animation-${transition}`;
const cached = cache[cacheKey];
if (cached) {
return cached;
let options;
if (chart.options.animation !== false) {
const config = me.chart.config;
- const scopeKeys = config.datasetAnimationScopeKeys(me._type);
- const scopes = config.getOptionScopes(me.getDataset().animation, scopeKeys);
- options = config.createResolver(scopes, me.getContext(index, active, mode));
+ const scopeKeys = config.datasetAnimationScopeKeys(me._type, transition);
+ const scopes = config.getOptionScopes(me.getDataset(), scopeKeys);
+ options = config.createResolver(scopes, me.getContext(index, active, transition));
}
- const animations = new Animations(chart, options && options[mode] || options);
+ const animations = new Animations(chart, options && options.animations);
if (options && options._cacheable) {
cache[cacheKey] = Object.freeze(animations);
}
defaults.route('scale.gridLines', 'color', '', 'borderColor');
defaults.route('scale.scaleLabel', 'color', '', 'color');
-defaults.describe('scales', {
- _fallback: 'scale',
+defaults.describe('scale', {
+ _fallback: false,
_scriptable: (name) => !name.startsWith('before') && !name.startsWith('after') && name !== 'callback' && name !== 'parser',
_indexable: (name) => name !== 'borderDash' && name !== 'tickBorderDash',
});
+defaults.describe('scales', {
+ _fallback: 'scale',
+});
+
/**
* Returns a new array containing numItems from arr
* @param {any[]} arr
-import {defined, isArray, isFunction, isObject, resolveObjectKey, valueOrDefault, _capitalize} from './helpers.core';
+import {defined, isArray, isFunction, isObject, resolveObjectKey, _capitalize} from './helpers.core';
/**
* Creates a Proxy for resolving raw values for options.
* @param {object[]} scopes - The option scopes to look for values, in resolution order
* @param {string[]} [prefixes] - The prefixes for values, in resolution order.
+ * @param {object[]} [rootScopes] - The root option scopes
+ * @param {string|boolean} [fallback] - Parent scopes fallback
* @returns Proxy
* @private
*/
-export function _createResolver(scopes, prefixes = ['']) {
+export function _createResolver(scopes, prefixes = [''], rootScopes = scopes, fallback) {
+ if (!defined(fallback)) {
+ fallback = _resolve('_fallback', scopes);
+ }
const cache = {
[Symbol.toStringTag]: 'Object',
_cacheable: true,
_scopes: scopes,
- override: (scope) => _createResolver([scope, ...scopes], prefixes),
+ _rootScopes: rootScopes,
+ _fallback: fallback,
+ override: (scope) => _createResolver([scope, ...scopes], prefixes, rootScopes, fallback),
};
return new Proxy(cache, {
/**
*/
get(target, prop) {
return _cached(target, prop,
- () => _resolveWithPrefixes(prop, prefixes, scopes));
+ () => _resolveWithPrefixes(prop, prefixes, scopes, target));
},
/**
_stack.delete(prop);
if (isObject(value)) {
// When scriptable option returns an object, create a resolver on that.
- value = createSubResolver(_proxy._scopes, prop, value);
+ value = createSubResolver(_proxy._scopes, _proxy, prop, value);
}
return value;
}
const scopes = _proxy._scopes.filter(s => s !== arr);
value = [];
for (const item of arr) {
- const resolver = createSubResolver(scopes, prop, item);
+ const resolver = createSubResolver(scopes, _proxy, prop, item);
value.push(_attachContext(resolver, _context, _subProxy && _subProxy[prop]));
}
}
return value;
}
-function createSubResolver(parentScopes, prop, value) {
- const set = new Set([value]);
- const lookupScopes = [value, ...parentScopes];
- const {keys, includeParents} = _resolveSubKeys(lookupScopes, prop, value);
- while (keys.length) {
- const key = keys.shift();
- for (const item of lookupScopes) {
- const scope = resolveObjectKey(item, key);
- if (scope) {
- set.add(scope);
- // fallback detour?
- const fallback = scope._fallback;
- if (defined(fallback)) {
- keys.push(...resolveFallback(fallback, key, scope).filter(k => k !== key));
- }
+function resolveFallback(fallback, prop, value) {
+ return isFunction(fallback) ? fallback(prop, value) : fallback;
+}
- } else if (key !== prop && scope === false) {
- // If any of the fallback scopes is explicitly false, return false
- // For example, options.hover falls back to options.interaction, when
- // options.interaction is false, options.hover will also resolve as false.
- return false;
+const getScope = (key, parent) => key === true ? parent : resolveObjectKey(parent, key);
+
+function addScopes(set, parentScopes, key, parentFallback) {
+ for (const parent of parentScopes) {
+ const scope = getScope(key, parent);
+ if (scope) {
+ set.add(scope);
+ const fallback = scope._fallback;
+ if (defined(fallback) && fallback !== key && fallback !== parentFallback) {
+ // When we reach the descriptor that defines a new _fallback, return that.
+ // The fallback will resume to that new scope.
+ return fallback;
}
+ } else if (scope === false && key !== 'fill') {
+ // Fallback to `false` results to `false`, expect for `fill`.
+ // The special case (fill) should be handled through descriptors.
+ return null;
}
}
- if (includeParents) {
- parentScopes.forEach(set.add, set);
- }
- return _createResolver([...set]);
-}
-
-function resolveFallback(fallback, prop, value) {
- const resolved = isFunction(fallback) ? fallback(prop, value) : fallback;
- return isArray(resolved) ? resolved : typeof resolved === 'string' ? [resolved] : [];
+ return false;
}
-function _resolveSubKeys(parentScopes, prop, value) {
- const fallback = valueOrDefault(_resolve('_fallback', parentScopes.map(scope => scope[prop] || scope)), true);
- const keys = [prop];
- if (defined(fallback)) {
- keys.push(...resolveFallback(fallback, prop, value));
+function createSubResolver(parentScopes, resolver, prop, value) {
+ const rootScopes = resolver._rootScopes;
+ const fallback = resolveFallback(resolver._fallback, prop, value);
+ const allScopes = [...parentScopes, ...rootScopes];
+ const set = new Set([value]);
+ let key = prop;
+ while (key !== false) {
+ key = addScopes(set, allScopes, key, fallback);
+ if (key === null) {
+ return false;
+ }
}
- return {keys: keys.filter(v => v), includeParents: fallback !== false && fallback !== prop};
+ if (defined(fallback) && fallback !== prop) {
+ const fallbackScopes = allScopes;
+ key = fallback;
+ while (key !== false) {
+ key = addScopes(set, fallbackScopes, key, fallback);
+ }
+ }
+ return _createResolver([...set], [''], rootScopes, fallback);
}
-function _resolveWithPrefixes(prop, prefixes, scopes) {
+
+function _resolveWithPrefixes(prop, prefixes, scopes, proxy) {
let value;
for (const prefix of prefixes) {
value = _resolve(readKey(prefix, prop), scopes);
if (defined(value)) {
- return (needsSubResolver(prop, value))
- ? createSubResolver(scopes, prop, value)
+ return needsSubResolver(prop, value)
+ ? createSubResolver(scopes, proxy, prop, value)
: value;
}
}
const chart = me._chart;
const options = me.options;
- const opts = options.enabled && chart.options.animation && options.animation;
+ const opts = options.enabled && chart.options.animation && options.animations;
const animations = new Animations(me._chart, opts);
- me._cachedAnimations = Object.freeze(animations);
+ if (opts._cacheable) {
+ me._cachedAnimations = Object.freeze(animations);
+ }
return animations;
}
animation: {
duration: 400,
easing: 'easeOutQuart',
+ },
+ animations: {
numbers: {
type: 'number',
properties: ['x', 'y', 'width', 'height', 'caretX', 'caretY'],
callbacks: {
_scriptable: false,
_indexable: false,
+ },
+ animation: {
+ _fallback: false
+ },
+ animations: {
+ _fallback: 'animation'
}
},
});
describe('_resolveAnimations', function() {
+ function animationsExpectations(anims, props) {
+ for (const [prop, opts] of Object.entries(props)) {
+ const anim = anims._properties.get(prop);
+ expect(anim).withContext(prop).toBeInstanceOf(Object);
+ if (anim) {
+ for (const [name, value] of Object.entries(opts)) {
+ expect(anim[name]).withContext('"' + name + '" of ' + JSON.stringify(anim)).toEqual(value);
+ }
+ }
+ }
+ }
+
it('should resolve to empty Animations when globally disabled', function() {
const chart = acquireChart({
type: 'line',
expect(controller._resolveAnimations(0)._properties.size).toEqual(0);
});
+
+ it('should fallback properly', function() {
+ const chart = acquireChart({
+ type: 'line',
+ data: {
+ datasets: [{
+ data: [1],
+ animation: {
+ duration: 200
+ }
+ }, {
+ type: 'bar',
+ data: [2]
+ }]
+ },
+ options: {
+ animation: {
+ delay: 100
+ },
+ animations: {
+ x: {
+ delay: 200
+ }
+ },
+ transitions: {
+ show: {
+ x: {
+ delay: 300
+ }
+ }
+ },
+ datasets: {
+ bar: {
+ animation: {
+ duration: 500
+ }
+ }
+ }
+ }
+ });
+ const controller = chart.getDatasetMeta(0).controller;
+
+ expect(Chart.defaults.animation.duration).toEqual(1000);
+
+ const def0 = controller._resolveAnimations(0, 'default', false);
+ animationsExpectations(def0, {
+ x: {
+ delay: 200,
+ duration: 200
+ },
+ y: {
+ delay: 100,
+ duration: 200
+ }
+ });
+
+ const controller2 = chart.getDatasetMeta(1).controller;
+ const def1 = controller2._resolveAnimations(0, 'default', false);
+ animationsExpectations(def1, {
+ x: {
+ delay: 200,
+ duration: 500
+ }
+ });
+ });
});
});
expect(resolver.hoverColor).toEqual(defaults.hoverColor);
});
- it('should resolve to parent scopes', function() {
+ it('should resolve to parent scopes, when _fallback is true', function() {
+ const descriptors = {
+ _fallback: true
+ };
const defaults = {
root: true,
sub: {
child: 'sub default comes before this',
opt: 'opt'
};
- const resolver = _createResolver([options, defaults]);
+ const resolver = _createResolver([options, defaults, descriptors]);
const sub = resolver.sub;
expect(sub.root).toEqual(true);
expect(sub.child).toEqual(true);
});
});
- it('should not fallback when _fallback is false', function() {
+ it('should not fallback by default', function() {
const defaults = {
hover: {
- _fallback: false,
a: 'defaults.hover'
},
controllers: {
});
it('should fallback throuhg multiple routes', function() {
+ const descriptors = {
+ _fallback: 'level1',
+ level1: {
+ _fallback: 'root'
+ },
+ level2: {
+ _fallback: 'level1'
+ }
+ };
const defaults = {
root: {
a: 'root'
},
level1: {
- _fallback: 'root',
b: 'level1',
},
level2: {
- _fallback: 'level1',
level1: {
g: 'level2.level1'
},
}
}
};
- const resolver = _createResolver([defaults]);
+ const resolver = _createResolver([defaults, descriptors]);
expect(resolver.level1).toEqualOptions({
a: 'root',
b: 'level1',
expect(resolver.level2.sublevel1).toEqualOptions({
a: 'root',
b: 'level1',
- c: 'level2', // TODO: this should be undefined
+ c: undefined,
d: 'sublevel1',
e: undefined,
f: undefined,
expect(resolver.level2.sublevel2).toEqualOptions({
a: 'root',
b: 'level1',
- c: 'level2', // TODO: this should be undefined
+ c: undefined,
d: undefined,
e: 'sublevel2',
f: undefined,
expect(resolver.level2.sublevel2.level1).toEqualOptions({
a: 'root',
b: 'level1',
- c: 'level2', // TODO: this should be undefined
+ c: undefined,
d: undefined,
- e: 'sublevel2', // TODO: this should be undefined
+ e: undefined,
f: 'sublevel2.level1',
- g: 'level2.level1'
+ g: undefined // same key only included from immediate parents and root
+ });
+ });
+
+ it('should fallback through multiple routes (animations)', function() {
+ const descriptors = {
+ animations: {
+ _fallback: 'animation',
+ },
+ };
+ const defaults = {
+ animation: {
+ duration: 1000,
+ easing: 'easeInQuad'
+ },
+ animations: {
+ colors: {
+ properties: ['color', 'backgroundColor'],
+ type: 'color'
+ },
+ numbers: {
+ properties: ['x', 'y'],
+ type: 'number'
+ }
+ },
+ transitions: {
+ resize: {
+ animation: {
+ duration: 0
+ }
+ },
+ show: {
+ animation: {
+ duration: 400
+ },
+ animations: {
+ colors: {
+ from: 'transparent'
+ }
+ }
+ }
+ }
+ };
+ const options = {
+ animation: {
+ easing: 'linear'
+ },
+ animations: {
+ colors: {
+ properties: ['color', 'borderColor', 'backgroundColor'],
+ },
+ duration: {
+ properties: ['a', 'b'],
+ type: 'boolean'
+ }
+ }
+ };
+
+ const show = _createResolver([options, defaults.transitions.show, defaults, descriptors]);
+ expect(show.animation).toEqualOptions({
+ duration: 400,
+ easing: 'linear'
+ });
+ expect(show.animations.colors._scopes).toEqual([
+ options.animations.colors,
+ defaults.transitions.show.animations.colors,
+ defaults.animations.colors,
+ options.animation,
+ defaults.transitions.show.animation,
+ defaults.animation
+ ]);
+ expect(show.animations.colors).toEqualOptions({
+ duration: 400,
+ from: 'transparent',
+ easing: 'linear',
+ type: 'color',
+ properties: ['color', 'borderColor', 'backgroundColor']
});
+ expect(show.animations.duration).toEqualOptions({
+ duration: 400,
+ easing: 'linear',
+ type: 'boolean',
+ properties: ['a', 'b']
+ });
+ expect(Object.getOwnPropertyNames(show.animations).filter(k => Chart.helpers.isObject(show.animations[k]))).toEqual([
+ 'colors',
+ 'duration',
+ 'numbers',
+ ]);
+ const def = _createResolver([options, defaults, descriptors]);
+ expect(def.animation).toEqualOptions({
+ duration: 1000,
+ easing: 'linear'
+ });
+ expect(def.animations.colors._scopes).toEqual([
+ options.animations.colors,
+ defaults.animations.colors,
+ options.animation,
+ defaults.animation
+ ]);
+ expect(def.animations.colors).toEqualOptions({
+ duration: 1000,
+ easing: 'linear',
+ type: 'color',
+ properties: ['color', 'borderColor', 'backgroundColor']
+ });
+ expect(def.animations.duration).toEqualOptions({
+ duration: 1000,
+ easing: 'linear',
+ type: 'boolean',
+ properties: ['a', 'b']
+ });
+ expect(Object.getOwnPropertyNames(def.animations).filter(k => Chart.helpers.isObject(show.animations[k]))).toEqual([
+ 'colors',
+ 'duration',
+ 'numbers',
+ ]);
});
+
});
});
onHover(event: ChartEvent, elements: ActiveElement[], chart: Chart): void;
}
-export interface CoreChartOptions extends ParsingOptions {
- animation: Scriptable<AnimationOptions | false, ScriptableContext>;
+export interface CoreChartOptions extends ParsingOptions, AnimationOptions {
- datasets: {
- animation: Scriptable<AnimationOptions | false, ScriptableContext>;
- };
+ datasets: AnimationOptions;
/**
* The base axis of the chart. 'x' for vertical charts and 'y' for horizontal charts.
| 'easeOutBounce'
| 'easeInOutBounce';
-export interface AnimationCommonSpec {
+export type AnimationSpec = {
/**
* The number of milliseconds an animation takes.
* @default 1000
*/
- duration: number;
+ duration: Scriptable<number, ScriptableContext>;
/**
* Easing function to use
* @default 'easeOutQuart'
*/
- easing: EasingFunction;
+ easing: Scriptable<EasingFunction, ScriptableContext>;
/**
* Running animation count + FPS display in upper left corner of the chart.
* @default false
*/
- debug: boolean;
+ debug: Scriptable<boolean, ScriptableContext>;
/**
* Delay before starting the animations.
* @default 0
*/
- delay: number;
+ delay: Scriptable<number, ScriptableContext>;
/**
* If set to true, the animations loop endlessly.
* @default false
*/
- loop: boolean;
+ loop: Scriptable<boolean, ScriptableContext>;
}
-export interface AnimationPropertySpec extends AnimationCommonSpec {
- properties: string[];
+export type AnimationsSpec = {
+ [name: string]: AnimationSpec & {
+ properties: string[];
- /**
- * Type of property, determines the interpolator used. Possible values: 'number', 'color' and 'boolean'. Only really needed for 'color', because typeof does not get that right.
- */
- type: 'color' | 'number' | 'boolean';
+ /**
+ * Type of property, determines the interpolator used. Possible values: 'number', 'color' and 'boolean'. Only really needed for 'color', because typeof does not get that right.
+ */
+ type: 'color' | 'number' | 'boolean';
- fn: <T>(from: T, to: T, factor: number) => T;
+ fn: <T>(from: T, to: T, factor: number) => T;
- /**
- * Start value for the animation. Current value is used when undefined
- */
- from: Color | number | boolean;
- /**
- *
- */
- to: Color | number | boolean;
+ /**
+ * Start value for the animation. Current value is used when undefined
+ */
+ from: Scriptable<Color | number | boolean, ScriptableContext>;
+ /**
+ *
+ */
+ to: Scriptable<Color | number | boolean, ScriptableContext>;
+ } | false
}
-export type AnimationSpecContainer = AnimationCommonSpec & {
- [prop: string]: AnimationPropertySpec | false;
-};
+export type TransitionSpec = {
+ animation: AnimationSpec;
+ animations: AnimationsSpec;
+}
-export type AnimationOptions = AnimationSpecContainer & {
- /**
- * Callback called on each step of an animation.
- */
- onProgress: (this: Chart, event: AnimationEvent) => void;
- /**
- * Callback called when all animations are completed.
- */
- onComplete: (this: Chart, event: AnimationEvent) => void;
+export type TransitionsSpec = {
+ [mode: string]: TransitionSpec
+}
- active: AnimationSpecContainer | false;
- hide: AnimationSpecContainer | false;
- reset: AnimationSpecContainer | false;
- resize: AnimationSpecContainer | false;
- show: AnimationSpecContainer | false;
+export type AnimationOptions = {
+ animation: AnimationSpec & {
+ /**
+ * Callback called on each step of an animation.
+ */
+ onProgress: (this: Chart, event: AnimationEvent) => void;
+ /**
+ * Callback called when all animations are completed.
+ */
+ onComplete: (this: Chart, event: AnimationEvent) => void;
+ };
+ animations: AnimationsSpec;
+ transitions: TransitionsSpec;
};
export interface FontSpec {
*/
textDirection: string;
- animation: Scriptable<AnimationSpecContainer, ScriptableContext>;
+ animation: AnimationSpec;
+
+ animations: AnimationsSpec;
callbacks: TooltipCallbacks;
}