mirror of
https://github.com/chartjs/Chart.js.git
synced 2025-12-08 20:36:08 +00:00
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:
parent
284e357fd3
commit
5d5e48d01b
@ -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><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
|
||||
|
||||
@ -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:
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -520,7 +520,7 @@ BarController.defaults = {
|
||||
datasets: {
|
||||
categoryPercentage: 0.8,
|
||||
barPercentage: 0.9,
|
||||
animation: {
|
||||
animations: {
|
||||
numbers: {
|
||||
type: 'number',
|
||||
properties: ['x', 'y', 'base', 'width', 'height']
|
||||
|
||||
@ -133,8 +133,9 @@ BubbleController.id = 'bubble';
|
||||
BubbleController.defaults = {
|
||||
datasetElementType: false,
|
||||
dataElementType: 'point',
|
||||
animation: {
|
||||
animations: {
|
||||
numbers: {
|
||||
type: 'number',
|
||||
properties: ['x', 'y', 'borderWidth', 'radius']
|
||||
}
|
||||
},
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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`,
|
||||
''
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
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.
|
||||
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;
|
||||
}
|
||||
}
|
||||
return 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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
54
types/index.esm.d.ts
vendored
@ -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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user