Isolate properties / modes from animation options (#8332)

* Isolate properties / modes from animation options
* tabs, something wrong with the linter
* Update misleading variable name
This commit is contained in:
Jukka Kurkela 2021-02-20 16:02:22 +02:00 committed by GitHub
parent 284e357fd3
commit 5d5e48d01b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 570 additions and 310 deletions

View File

@ -33,7 +33,7 @@ function example() {
}]
},
options: {
animation: {
animations: {
tension: {
duration: 1000,
easing: 'linear',
@ -77,7 +77,7 @@ function example() {
}]
},
options: {
animation: {
transitions: {
show: {
x: {
from: 0
@ -107,10 +107,30 @@ function example() {
</TabItem>
</Tabs>
## Animation Configuration
## Animation configuration
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`.
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:
* `` - 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
| ---- | ---- | ------- | -----------
@ -119,84 +139,65 @@ Namespace: `options.animation`, the global options are defined in `Chart.defaul
| `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>&lt;T&gt;(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

View File

@ -28,7 +28,7 @@ myLineChart.data.datasets[0].data[2] = 50; // Would update the first dataset's v
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:

View File

@ -33,13 +33,13 @@
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(),
@ -50,7 +50,8 @@
randomScalingFactor(),
randomScalingFactor()
],
fill: false,
fill: 1,
tension: 0.5
}, {
label: 'My Second dataset',
fill: false,
@ -68,10 +69,17 @@
}]
},
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,

View File

@ -62,7 +62,7 @@
}]
},
options: {
animation: {
animations: {
radius: {
duration: 400,
easing: 'linear',
@ -74,23 +74,18 @@
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,

View File

@ -520,7 +520,7 @@ BarController.defaults = {
datasets: {
categoryPercentage: 0.8,
barPercentage: 0.9,
animation: {
animations: {
numbers: {
type: 'number',
properties: ['x', 'y', 'base', 'width', 'height']

View File

@ -133,8 +133,9 @@ BubbleController.id = 'bubble';
BubbleController.defaults = {
datasetElementType: false,
dataElementType: 'point',
animation: {
animations: {
numbers: {
type: 'number',
properties: ['x', 'y', 'borderWidth', 'radius']
}
},

View File

@ -329,15 +329,17 @@ DoughnutController.defaults = {
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: {

View File

@ -122,12 +122,14 @@ PolarAreaController.id = 'polarArea';
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',

View File

@ -28,7 +28,7 @@ export default class Animation {
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;

View File

@ -1,20 +1,31 @@
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,
});
// Property sets
const animationOptions = Object.keys(defaults.animation);
defaults.describe('animation', {
_fallback: false,
_indexable: false,
_scriptable: (name) => name !== 'onProgress' && name !== 'onComplete' && name !== 'fn',
});
defaults.set('animations', {
colors: {
type: 'color',
properties: colors
@ -23,29 +34,37 @@ defaults.set('animation', {
type: 'number',
properties: numbers
},
});
// Update modes. These are overrides / additions to the above animations.
defaults.describe('animations', {
_fallback: 'animation',
});
defaults.set('transitions', {
active: {
animation: {
duration: 400
}
},
resize: {
animation: {
duration: 0
}
},
show: {
animations: {
colors: {
type: 'color',
properties: colors,
from: 'transparent'
},
visible: {
type: 'boolean',
duration: 0 // show immediately
},
}
},
hide: {
animations: {
colors: {
type: 'color',
properties: colors,
to: 'transparent'
},
visible: {
@ -53,30 +72,25 @@ defaults.set('animation', {
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;
}
@ -85,7 +99,7 @@ export default class Animations {
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);
}

View File

@ -185,15 +185,20 @@ export default class Config {
* 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) {
return cachedKeys(`${datasetType}.animation`,
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`,
''
]);
}

View File

@ -763,11 +763,11 @@ export default class DatasetController {
/**
* @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;
@ -775,11 +775,11 @@ export default class DatasetController {
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);
}

View File

@ -83,12 +83,16 @@ defaults.route('scale.ticks', 'color', '', 'color');
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

View File

@ -1,18 +1,25 @@
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, {
/**
@ -20,7 +27,7 @@ export function _createResolver(scopes, prefixes = ['']) {
*/
get(target, prop) {
return _cached(target, prop,
() => _resolveWithPrefixes(prop, prefixes, scopes));
() => _resolveWithPrefixes(prop, prefixes, scopes, target));
},
/**
@ -186,7 +193,7 @@ function _resolveScriptable(prop, value, target, receiver) {
_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;
}
@ -202,64 +209,69 @@ function _resolveArray(prop, value, target, isIndexable) {
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);
function resolveFallback(fallback, prop, value) {
return isFunction(fallback) ? fallback(prop, value) : fallback;
}
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);
// fallback detour?
const fallback = scope._fallback;
if (defined(fallback)) {
keys.push(...resolveFallback(fallback, key, scope).filter(k => k !== key));
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;
}
}
return false;
}
} 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.
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;
}
}
if (defined(fallback) && fallback !== prop) {
const fallbackScopes = allScopes;
key = fallback;
while (key !== false) {
key = addScopes(set, fallbackScopes, key, fallback);
}
if (includeParents) {
parentScopes.forEach(set.add, set);
}
return _createResolver([...set]);
return _createResolver([...set], [''], rootScopes, fallback);
}
function resolveFallback(fallback, prop, value) {
const resolved = isFunction(fallback) ? fallback(prop, value) : fallback;
return isArray(resolved) ? resolved : typeof resolved === 'string' ? [resolved] : [];
}
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));
}
return {keys: keys.filter(v => v), includeParents: fallback !== false && fallback !== prop};
}
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;
}
}

View File

@ -385,9 +385,11 @@ export class Tooltip extends Element {
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);
if (opts._cacheable) {
me._cachedAnimations = Object.freeze(animations);
}
return animations;
}
@ -1108,6 +1110,8 @@ export default {
animation: {
duration: 400,
easing: 'easeOutQuart',
},
animations: {
numbers: {
type: 'number',
properties: ['x', 'y', 'width', 'height', 'caretX', 'caretY'],
@ -1203,6 +1207,12 @@ export default {
callbacks: {
_scriptable: false,
_indexable: false,
},
animation: {
_fallback: false
},
animations: {
_fallback: 'animation'
}
},

View File

@ -742,6 +742,18 @@ describe('Chart.DatasetController', function() {
});
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',
@ -778,5 +790,70 @@ describe('Chart.DatasetController', function() {
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
}
});
});
});
});

View File

@ -17,7 +17,10 @@ describe('Chart.helpers.config', function() {
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: {
@ -28,7 +31,7 @@ describe('Chart.helpers.config', function() {
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);
@ -125,10 +128,9 @@ describe('Chart.helpers.config', function() {
});
});
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: {
@ -252,16 +254,23 @@ describe('Chart.helpers.config', function() {
});
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'
},
@ -277,7 +286,7 @@ describe('Chart.helpers.config', function() {
}
}
};
const resolver = _createResolver([defaults]);
const resolver = _createResolver([defaults, descriptors]);
expect(resolver.level1).toEqualOptions({
a: 'root',
b: 'level1',
@ -292,7 +301,7 @@ describe('Chart.helpers.config', function() {
expect(resolver.level2.sublevel1).toEqualOptions({
a: 'root',
b: 'level1',
c: 'level2', // TODO: this should be undefined
c: undefined,
d: 'sublevel1',
e: undefined,
f: undefined,
@ -301,7 +310,7 @@ describe('Chart.helpers.config', function() {
expect(resolver.level2.sublevel2).toEqualOptions({
a: 'root',
b: 'level1',
c: 'level2', // TODO: this should be undefined
c: undefined,
d: undefined,
e: 'sublevel2',
f: undefined,
@ -310,13 +319,129 @@ describe('Chart.helpers.config', function() {
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',
]);
});
});
});

54
types/index.esm.d.ts vendored
View File

@ -1335,12 +1335,9 @@ export interface HoverInteractionOptions extends CoreInteractionOptions {
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.
@ -1460,38 +1457,39 @@ export type EasingFunction =
| '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 {
export type AnimationsSpec = {
[name: string]: AnimationSpec & {
properties: string[];
/**
@ -1504,18 +1502,25 @@ export interface AnimationPropertySpec extends AnimationCommonSpec {
/**
* Start value for the animation. Current value is used when undefined
*/
from: Color | number | boolean;
from: Scriptable<Color | number | boolean, ScriptableContext>;
/**
*
*/
to: Color | number | boolean;
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 & {
export type TransitionsSpec = {
[mode: string]: TransitionSpec
}
export type AnimationOptions = {
animation: AnimationSpec & {
/**
* Callback called on each step of an animation.
*/
@ -1524,12 +1529,9 @@ export type AnimationOptions = AnimationSpecContainer & {
* Callback called when all animations are completed.
*/
onComplete: (this: Chart, event: AnimationEvent) => void;
active: AnimationSpecContainer | false;
hide: AnimationSpecContainer | false;
reset: AnimationSpecContainer | false;
resize: AnimationSpecContainer | false;
show: AnimationSpecContainer | false;
};
animations: AnimationsSpec;
transitions: TransitionsSpec;
};
export interface FontSpec {
@ -2452,7 +2454,9 @@ export interface TooltipOptions extends CoreInteractionOptions {
*/
textDirection: string;
animation: Scriptable<AnimationSpecContainer, ScriptableContext>;
animation: AnimationSpec;
animations: AnimationsSpec;
callbacks: TooltipCallbacks;
}