]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Isolate properties / modes from animation options (#8332)
authorJukka Kurkela <jukka.kurkela@gmail.com>
Sat, 20 Feb 2021 14:02:22 +0000 (16:02 +0200)
committerGitHub <noreply@github.com>
Sat, 20 Feb 2021 14:02:22 +0000 (09:02 -0500)
* Isolate properties / modes from animation options
* tabs, something wrong with the linter
* Update misleading variable name

18 files changed:
docs/docs/configuration/animations.mdx
docs/docs/developers/api.md
samples/animations/drop.html
samples/animations/loop.html
src/controllers/controller.bar.js
src/controllers/controller.bubble.js
src/controllers/controller.doughnut.js
src/controllers/controller.polarArea.js
src/core/core.animation.js
src/core/core.animations.js
src/core/core.config.js
src/core/core.datasetController.js
src/core/core.scale.js
src/helpers/helpers.config.js
src/plugins/plugin.tooltip.js
test/specs/core.datasetController.tests.js
test/specs/helpers.config.tests.js
types/index.esm.d.ts

index c1ed00497c2e216f0cd439dee4402f5a388235de..f3ca5e30b51e9b7d1716d1587f406ea2626ebe78 100644 (file)
@@ -33,21 +33,21 @@ function example() {
         }]
       },
       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);
@@ -77,24 +77,24 @@ function example() {
         }]
       },
       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);
@@ -107,10 +107,30 @@ function example() {
 </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
 | ---- | ---- | ------- | -----------
@@ -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
index 60d1c6d089a7cc7a5dce45f2a41430b7b2345ca9..0cb9841f1d4f919434557b6530c9c6e11e03fca4 100644 (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:
 
index 8af8d0178ab15f49781c249f50eb31110d4d43b1..b89b978a6038c627bdf66c3942c123387111e495 100644 (file)
                                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,
                                }]
                        },
                        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,
index 9356a3f3a6351b961c1983a567b4b5201d06815b..d75e47f1b6f5361aa4578b22c22e0077b6846aed 100644 (file)
@@ -62,7 +62,7 @@
                                }]
                        },
                        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,
index 1dfcc44df6c1b1728e9bddbdc51c5235a5a21843..1782d91279869153788026096c7a295d5b14a287 100644 (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']
index f1f0e1ecfa0115b4c7126140be54d96382eaef29..348ea8866d7f1f9f3270d05b713d17cebe519e71 100644 (file)
@@ -133,8 +133,9 @@ BubbleController.id = 'bubble';
 BubbleController.defaults = {
   datasetElementType: false,
   dataElementType: 'point',
-  animation: {
+  animations: {
     numbers: {
+      type: 'number',
       properties: ['x', 'y', 'borderWidth', 'radius']
     }
   },
index e10fa42b97bec4a037c15e79a00283d935ebb2db..c283a266a656edaea7d1adeeeb04bbe87fd9040c 100644 (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: {
index a476c275a2454ef4114749966cb6dec34e410c4c..49ac298987c19d038310cd0856ec19ff032f247b 100644 (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',
index 7be74bcef5dca029c427759a2f3677e798685562..dc78afd79b0b110b77cb017376f3a51cf3936c87 100644 (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;
index ff688b6c9895f97b226e20f9f30ac99fa5edf887..600aa71e1b636c7211b97c2d3d0946796d90261a 100644 (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,
+});
+
+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
@@ -23,60 +34,63 @@ defaults.set('animation', {
     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;
       }
@@ -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);
         }
index 8a47782c9f2657f24900a43839d2a4f2c2a01a00..580082b292455b6d0b0dff28d98b7e463b070be3 100644 (file)
@@ -166,11 +166,11 @@ export default class Config {
   }
 
   /**
-        * 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,
       () => [
@@ -182,29 +182,34 @@ 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
-        * @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}`,
       () => [
@@ -219,7 +224,7 @@ export default class Config {
   /**
    * 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;
@@ -233,11 +238,11 @@ export default class Config {
   }
 
   /**
-        * 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) {
@@ -267,9 +272,9 @@ export default class Config {
   }
 
   /**
-        * Returns the option scopes for resolving chart options
-        * @return {object[]}
-        */
+   * Returns the option scopes for resolving chart options
+   * @return {object[]}
+   */
   chartOptionScopes() {
     return [
       this.options,
@@ -281,12 +286,12 @@ export default class Config {
   }
 
   /**
-        * @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);
@@ -306,10 +311,10 @@ export default class Config {
   }
 
   /**
-        * @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)
@@ -342,7 +347,7 @@ function needContext(proxy, names) {
 
   for (const prop of names) {
     if ((isScriptable(prop) && isFunction(proxy[prop]))
-                       || (isIndexable(prop) && isArray(proxy[prop]))) {
+      || (isIndexable(prop) && isArray(proxy[prop]))) {
       return true;
     }
   }
index 615c55ddc970ec3e1b4d42c9b0ea491358268ece..07b1795ba95704a7fe9b09ad418a60b01694040b 100644 (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);
     }
index 26181bb9cf5bd37c5835c744d79379a334fa8d74..fe468b9adb4a40ecdb41fb44e42595b3fb06467d 100644 (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
index a785fa39a5abe31c552b664f170ad3db0a734a8f..3af16a66a1897a85c6bd240b2f1acd7bf16d363f 100644 (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);
-      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;
     }
   }
index 40b0fbbf62345bbe5b34fab8a36f38da2d430af3..1bc89d95cde446d74c0efe5ee4dd90324fff3098 100644 (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);
-    me._cachedAnimations = Object.freeze(animations);
+    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'
     }
   },
 
index 028b3077ffe2ee35ba7896cec5b7c2a4c027c7d9..c65510aad1bed47dc1760a2e574d0ff68c93040c 100644 (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
+        }
+      });
+    });
   });
 });
index b79c0b9b82265add1f148762ec2a44220d647104..86bee170e7cd0f3d5412efecdf2c1571566f8249 100644 (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',
+        ]);
       });
+
     });
   });
 
index 5cde43d05833d41a34750fc9954e20d6f788eb40..46c14653415cd7ced97c5a3be9d9704dbfc8e97c 100644 (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,76 +1457,81 @@ 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 {
-       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 {
@@ -2452,7 +2454,9 @@ export interface TooltipOptions extends CoreInteractionOptions {
         */
        textDirection: string;
 
-       animation: Scriptable<AnimationSpecContainer, ScriptableContext>;
+  animation: AnimationSpec;
+
+  animations: AnimationsSpec;
 
        callbacks: TooltipCallbacks;
 }