]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Option resolution with proxies (#8374)
authorJukka Kurkela <jukka.kurkela@gmail.com>
Mon, 15 Feb 2021 19:42:32 +0000 (21:42 +0200)
committerGitHub <noreply@github.com>
Mon, 15 Feb 2021 19:42:32 +0000 (14:42 -0500)
* Option resolution with proxies

* Remove plugin fallback to root options/defaults

* Update core plugins, reduntant font fallbacks

* Add some notes

45 files changed:
docs/docs/configuration/index.md
docs/docs/general/options.md
docs/docs/getting-started/v3-migration.md
samples/animations/delay.html
samples/animations/loop.html
src/controllers/controller.bar.js
src/controllers/controller.bubble.js
src/controllers/controller.doughnut.js
src/controllers/controller.line.js
src/controllers/controller.pie.js
src/controllers/controller.polarArea.js
src/controllers/controller.radar.js
src/core/core.animations.js
src/core/core.config.js
src/core/core.controller.js
src/core/core.datasetController.js
src/core/core.defaults.js
src/core/core.layouts.js
src/core/core.plugins.js
src/core/core.scale.js
src/core/core.typedRegistry.js
src/elements/element.arc.js
src/elements/element.bar.js
src/elements/element.line.js
src/elements/element.point.js
src/helpers/helpers.config.js [new file with mode: 0644]
src/helpers/helpers.core.js
src/helpers/index.js
src/platform/platform.base.js
src/platform/platform.dom.js
src/plugins/plugin.legend.js
src/plugins/plugin.title.js
src/plugins/plugin.tooltip.js
src/scales/scale.radialLinear.js
test/fixtures/scale.radialLinear/gridlines-scriptable.js
test/specs/controller.bar.tests.js
test/specs/core.animations.tests.js
test/specs/core.controller.tests.js
test/specs/core.datasetController.tests.js
test/specs/core.plugin.tests.js
test/specs/helpers.config.tests.js [new file with mode: 0644]
test/specs/plugin.legend.tests.js
test/specs/plugin.title.tests.js
test/specs/plugin.tooltip.tests.js
types/index.esm.d.ts

index 7abba9e838ddc763fe6d5783c5b107bd74fea331..ce8074f5c37442ee09912269461eb22f3fbcff88 100644 (file)
@@ -10,23 +10,23 @@ This concept was introduced in Chart.js 1.0 to keep configuration [DRY](https://
 
 Chart.js merges the options object passed to the chart with the global configuration using chart type defaults and scales defaults appropriately. This way you can be as specific as you would like in your individual chart configuration, while still changing the defaults for all chart types where applicable. The global general options are defined in `Chart.defaults`. The defaults for each chart type are discussed in the documentation for that chart type.
 
-The following example would set the hover mode to 'nearest' for all charts where this was not overridden by the chart type defaults or the options passed to the constructor on creation.
+The following example would set the interaction mode to 'nearest' for all charts where this was not overridden by the chart type defaults or the options passed to the constructor on creation.
 
 ```javascript
-Chart.defaults.hover.mode = 'nearest';
+Chart.defaults.interaction.mode = 'nearest';
 
-// Hover mode is set to nearest because it was not overridden here
-var chartHoverModeNearest = new Chart(ctx, {
+// Interaction mode is set to nearest because it was not overridden here
+var chartInteractionModeNearest = new Chart(ctx, {
     type: 'line',
     data: data
 });
 
-// This chart would have the hover mode that was passed in
-var chartDifferentHoverMode = new Chart(ctx, {
+// This chart would have the interaction mode that was passed in
+var chartDifferentInteractionMode = new Chart(ctx, {
     type: 'line',
     data: data,
     options: {
-        hover: {
+        interaction: {
             // Overrides the global setting
             mode: 'index'
         }
@@ -36,15 +36,7 @@ var chartDifferentHoverMode = new Chart(ctx, {
 
 ## Dataset Configuration
 
-Options may be configured directly on the dataset. The dataset options can be changed at 3 different levels and are evaluated with the following priority:
-
-- per dataset: dataset.*
-- per chart: options.datasets[type].*
-- or globally: Chart.defaults.controllers[type].datasets.*
-
-where type corresponds to the dataset type.
-
-*Note:* dataset options take precedence over element options.
+Options may be configured directly on the dataset. The dataset options can be changed at multiple different levels. See [options](../general/options.md#dataset-level-options) for details on how the options are resolved.
 
 The following example would set the `showLine` option to 'false' for all line datasets except for those overridden by options passed to the dataset on creation.
 
index 7776138532286b19830c59f220b915a791909d25..e5906a2fede244801b783ef91f3665f6c0dc1f59 100644 (file)
@@ -2,9 +2,74 @@
 title: Options
 ---
 
+## Option resolution
+
+Options are resolved from top to bottom, using a context dependent route.
+
+### Chart level options
+
+* options
+* defaults.controllers[`config.type`]
+* defaults
+
+### Dataset level options
+
+`dataset.type` defaults to `config.type`, if not specified.
+
+* dataset
+* options.datasets[`dataset.type`]
+* options.controllers[`dataset.type`].datasets
+* options
+* defaults.datasets[`dataset.type`]
+* defaults.controllers[`dataset.type`].datasets
+* defaults
+
+### Dataset animation options
+
+* dataset.animation
+* options.controllers[`dataset.type`].datasets.animation
+* options.animation
+* defaults.controllers[`dataset.type`].datasets.animation
+* defaults.animation
+
+### Dataset element level options
+
+Each scope is looked up with `elementType` prefix in the option name first, then wihtout the prefix. For example, `radius` for `point` element is looked up using `pointRadius` and if that does not hit, then `radius`.
+
+* dataset
+* options.datasets[`dataset.type`]
+* options.controllers[`dataset.type`].datasets
+* options.controllers[`dataset.type`].elements[`elementType`]
+* options.elements[`elementType`]
+* options
+* defaults.datasets[`dataset.type`]
+* defaults.controllers[`dataset.type`].datasets
+* defaults.controllers[`dataset.type`].elements[`elementType`]
+* defaults.elements[`elementType`]
+* defaults
+
+### Scale options
+
+* options.scales
+* defaults.controllers[`config.type`].scales
+* defaults.controllers[`dataset.type`].scales
+* defaults.scales
+
+### Plugin options
+
+A plugin can provide `additionalOptionScopes` array of paths to additionally look for its options in. For root scope, use empty string: `''`. Most core plugins also take options from root scope.
+
+* options.plugins[`plugin.id`]
+* options.controllers[`config.type`].plugins[`plugin.id`]
+* (options.[`...plugin.additionalOptionScopes`])
+* defaults.controllers[`config.type`].plugins[`plugin.id`]
+* defaults.plugins[`plugin.id`]
+* (defaults.[`...plugin.additionalOptionScopes`])
+
 ## Scriptable Options
 
 Scriptable options also accept a function which is called for each of the underlying data values and that takes the unique argument `context` representing contextual information (see [option context](options.md#option-context)).
+A resolver is passed as second parameter, that can be used to access other options in the same context.
 
 Example:
 
@@ -15,6 +80,10 @@ color: function(context) {
     return value < 0 ? 'red' :  // draw negative values in red
         index % 2 ? 'blue' :    // else, alternate values in blue and green
         'green';
+},
+borderColor: function(context, options) {
+    var color = options.color; // resolve the value of another scriptable option: 'red', 'blue' or 'green'
+    return Chart.helpers.color(color).lighten(0.2);
 }
 ```
 
@@ -64,6 +133,7 @@ In addition to [chart](#chart)
 * `dataset`: dataset at index `datasetIndex`
 * `datasetIndex`: index of the current dataset
 * `index`: getter for `datasetIndex`
+* `mode`: the update mode
 * `type`: `'dataset'`
 
 ### data
@@ -76,6 +146,7 @@ In addition to [dataset](#dataset)
 * `raw`: the raw data values for the given `dataIndex` and `datasetIndex`
 * `element`: the element (point, arc, bar, etc.) for this data
 * `index`: getter for `dataIndex`
+* `mode`: the update mode
 * `type`: `'data'`
 
 ### scale
index 6b86132ecf2abffa138e0b77f5364748b7881cb8..f028bf0977512abb08726129da99d8b482234d14 100644 (file)
@@ -63,6 +63,10 @@ A number of changes were made to the configuration options passed to the `Chart`
 
 * Indexable options are now looping. `backgroundColor: ['red', 'green']` will result in alternating `'red'` / `'green'` if there are more than 2 data points.
 * The input properties of object data can now be freely specified, see [data structures](../general/data-structures.md) for details.
+* Most options are resolved utilizing proxies, instead merging with defaults. In addition to easily enabling different resolution routes for different contexts, it allows using other resolved options in scriptable options.
+  * Options are by default scriptable and indexable, unless disabled for some reason.
+  * Scriptable options receive a option reolver as second parameter for accessing other options in same context.
+  * Resolution falls to upper scopes, if no match is found earlier. See [options](./general/options.md) for details.
 
 #### Specific changes
 
index 66b37a1236f046687f049f28a25b779d82de627f..aa81bd73bb76fa298275bfc98ef2914b631dddc1 100644 (file)
 
                };
                window.onload = function() {
+                       var delayed = false;
                        var ctx = document.getElementById('canvas').getContext('2d');
                        window.myBar = new Chart(ctx, {
                                type: 'bar',
                                data: barChartData,
                                options: {
-                                       animation: (context) => {
-                                               if (context.active) {
-                                                       return {
-                                                               duration: 400
-                                                       };
-                                               }
-                                               var delay = 0;
-                                               var dsIndex = context.datasetIndex;
-                                               var index = context.dataIndex;
-                                               if (context.parsed && !context.delayed) {
-                                                       delay = index * 300 + dsIndex * 100;
-                                                       context.delayed = true;
-                                               }
-                                               return {
-                                                       easing: 'linear',
-                                                       duration: 600,
-                                                       delay
-                                               };
+                                       animation: {
+                                               onComplete: () => {
+                                                       delayed = true;
+                                               },
+                                               delay: (context) => {
+                                                       let delay = 0;
+                                                       if (context.type === 'data' && context.mode === 'default' && !delayed) {
+                                                               delay = context.dataIndex * 300 + context.datasetIndex * 100;
+                                                       }
+                                                       return delay;
+                                               },
                                        },
                                        plugins: {
                                                title: {
index 81956b56d4055e86349899c04acfd9772de9540f..9356a3f3a6351b961c1983a567b4b5201d06815b 100644 (file)
                                }]
                        },
                        options: {
-                               animation: (context) => Object.assign({},
-                                       Chart.defaults.animation,
-                                       {
-                                               radius: {
-                                                       duration: 400,
-                                                       easing: 'linear',
-                                                       loop: context.active
-                                               }
+                               animation: {
+                                       radius: {
+                                               duration: 400,
+                                               easing: 'linear',
+                                               loop: (context) => context.active
                                        }
-                               ),
+                               },
                                elements: {
                                        point: {
                                                hoverRadius: 6
index 165421209d189ead751b328207358c8e3cc8c8b6..1dfcc44df6c1b1728e9bddbdc51c5235a5a21843 100644 (file)
@@ -266,9 +266,8 @@ export default class BarController extends DatasetController {
     me.updateSharedOptions(sharedOptions, mode, firstOpts);
 
     for (let i = start; i < start + count; i++) {
-      const options = sharedOptions || me.resolveDataElementOptions(i, mode);
-      const vpixels = me._calculateBarValuePixels(i, options);
-      const ipixels = me._calculateBarIndexPixels(i, ruler, options);
+      const vpixels = me._calculateBarValuePixels(i);
+      const ipixels = me._calculateBarIndexPixels(i, ruler);
 
       const properties = {
         horizontal,
@@ -280,7 +279,7 @@ export default class BarController extends DatasetController {
       };
 
       if (includeOptions) {
-        properties.options = options;
+        properties.options = sharedOptions || me.resolveDataElementOptions(i, mode);
       }
       me.updateElement(bars[i], i, properties, mode);
     }
@@ -400,11 +399,11 @@ export default class BarController extends DatasetController {
         * Note: pixel values are not clamped to the scale area.
         * @private
         */
-  _calculateBarValuePixels(index, options) {
+  _calculateBarValuePixels(index) {
     const me = this;
     const meta = me._cachedMeta;
     const vScale = meta.vScale;
-    const {base: baseValue, minBarLength} = options;
+    const {base: baseValue, minBarLength} = me.options;
     const parsed = me.getParsed(index);
     const custom = parsed._custom;
     const floating = isFloatBar(custom);
@@ -459,9 +458,10 @@ export default class BarController extends DatasetController {
   /**
         * @private
         */
-  _calculateBarIndexPixels(index, ruler, options) {
+  _calculateBarIndexPixels(index, ruler) {
     const me = this;
-    const stackCount = me.chart.options.skipNull ? me._getStackCount(index) : ruler.stackCount;
+    const options = me.options;
+    const stackCount = options.skipNull ? me._getStackCount(index) : ruler.stackCount;
     const range = options.barThickness === 'flex'
       ? computeFlexCategoryTraits(index, ruler, options, stackCount)
       : computeFitCategoryTraits(index, ruler, options, stackCount);
@@ -510,20 +510,7 @@ BarController.id = 'bar';
 BarController.defaults = {
   datasetElementType: false,
   dataElementType: 'bar',
-  dataElementOptions: [
-    'backgroundColor',
-    'borderColor',
-    'borderSkipped',
-    'borderWidth',
-    'borderRadius',
-    'barPercentage',
-    'barThickness',
-    'base',
-    'categoryPercentage',
-    'maxBarThickness',
-    'minBarLength',
-    'pointStyle'
-  ],
+
   interaction: {
     mode: 'index'
   },
index 7c8c7dcd7af04c338217b2e65ea2426f9ab52776..f1f0e1ecfa0115b4c7126140be54d96382eaef29 100644 (file)
@@ -1,6 +1,5 @@
 import DatasetController from '../core/core.datasetController';
-import {resolve} from '../helpers/helpers.options';
-import {resolveObjectKey} from '../helpers/helpers.core';
+import {resolveObjectKey, valueOrDefault} from '../helpers/helpers.core';
 
 export default class BubbleController extends DatasetController {
   initialize() {
@@ -107,29 +106,20 @@ export default class BubbleController extends DatasetController {
         * @protected
         */
   resolveDataElementOptions(index, mode) {
-    const me = this;
-    const chart = me.chart;
-    const parsed = me.getParsed(index);
+    const parsed = this.getParsed(index);
     let values = super.resolveDataElementOptions(index, mode);
 
-    // Scriptable options
-    const context = me.getContext(index, mode === 'active');
-
     // In case values were cached (and thus frozen), we need to clone the values
     if (values.$shared) {
       values = Object.assign({}, values, {$shared: false});
     }
 
-
     // Custom radius resolution
+    const radius = values.radius;
     if (mode !== 'active') {
       values.radius = 0;
     }
-    values.radius += resolve([
-      parsed && parsed._custom,
-      me._config.radius,
-      chart.options.elements.point.radius
-    ], context, index);
+    values.radius += valueOrDefault(parsed && parsed._custom, radius);
 
     return values;
   }
@@ -143,15 +133,6 @@ BubbleController.id = 'bubble';
 BubbleController.defaults = {
   datasetElementType: false,
   dataElementType: 'point',
-  dataElementOptions: [
-    'backgroundColor',
-    'borderColor',
-    'borderWidth',
-    'hitRadius',
-    'radius',
-    'pointStyle',
-    'rotation'
-  ],
   animation: {
     numbers: {
       properties: ['x', 'y', 'borderWidth', 'radius']
index 24e242fa30ef2beb2ebb1d8243fa31d846aeb329..e10fa42b97bec4a037c15e79a00283d935ebb2db 100644 (file)
@@ -81,14 +81,14 @@ export default class DoughnutController extends DatasetController {
         * @private
         */
   _getRotation() {
-    return toRadians(valueOrDefault(this._config.rotation, this.chart.options.rotation) - 90);
+    return toRadians(this.options.rotation - 90);
   }
 
   /**
         * @private
         */
   _getCircumference() {
-    return toRadians(valueOrDefault(this._config.circumference, this.chart.options.circumference));
+    return toRadians(this.options.circumference);
   }
 
   /**
@@ -124,10 +124,10 @@ export default class DoughnutController extends DatasetController {
   update(mode) {
     const me = this;
     const chart = me.chart;
-    const {chartArea, options} = chart;
+    const {chartArea} = chart;
     const meta = me._cachedMeta;
     const arcs = meta.data;
-    const cutout = options.cutoutPercentage / 100 || 0;
+    const cutout = me.options.cutoutPercentage / 100 || 0;
     const chartWeight = me._getRingWeight(me.index);
 
     // Compute the maximal rotation & circumference limits.
@@ -157,7 +157,7 @@ export default class DoughnutController extends DatasetController {
         */
   _circumference(i, reset) {
     const me = this;
-    const opts = me.chart.options;
+    const opts = me.options;
     const meta = me._cachedMeta;
     const circumference = me._getCircumference();
     return reset && opts.animation.animateRotate ? 0 : this.chart.getDataVisibility(i) ? me.calculateCircumference(meta._parsed[i] * circumference / TAU) : 0;
@@ -328,13 +328,6 @@ DoughnutController.id = 'doughnut';
 DoughnutController.defaults = {
   datasetElementType: false,
   dataElementType: 'arc',
-  dataElementOptions: [
-    'backgroundColor',
-    'borderColor',
-    'borderWidth',
-    'borderAlign',
-    'offset'
-  ],
   animation: {
     numbers: {
       type: 'number',
@@ -347,14 +340,18 @@ DoughnutController.defaults = {
   },
   aspectRatio: 1,
 
-  // The percentage of the chart that we cut out of the middle.
-  cutoutPercentage: 50,
+  datasets: {
+    // The percentage of the chart that we cut out of the middle.
+    cutoutPercentage: 50,
 
-  // The rotation of the chart, where the first data arc begins.
-  rotation: 0,
+    // The rotation of the chart, where the first data arc begins.
+    rotation: 0,
 
-  // The total circumference of the chart.
-  circumference: 360,
+    // The total circumference of the chart.
+    circumference: 360
+  },
+
+  indexAxis: 'r',
 
   // Need to override these to give a nice default
   plugins: {
index ed3f9319ed1324b9e1bd79dd2ed6c683ee0a6a14..9cbd48aa63f5bd9b20c09d4e0dcc81779821b9cf 100644 (file)
@@ -1,7 +1,5 @@
 import DatasetController from '../core/core.datasetController';
-import {valueOrDefault} from '../helpers/helpers.core';
 import {isNumber, _limitValue} from '../helpers/helpers.math';
-import {resolve} from '../helpers/helpers.options';
 import {_lookupByKey} from '../helpers/helpers.collection';
 
 export default class LineController extends DatasetController {
@@ -32,9 +30,13 @@ export default class LineController extends DatasetController {
 
     // In resize mode only point locations change, so no need to set the options.
     if (mode !== 'resize') {
+      const options = me.resolveDatasetElementOptions(mode);
+      if (!me.options.showLine) {
+        options.borderWidth = 0;
+      }
       me.updateElement(line, undefined, {
         animated: !animationsDisabled,
-        options: me.resolveDatasetElementOptions()
+        options
       }, mode);
     }
 
@@ -49,7 +51,7 @@ export default class LineController extends DatasetController {
     const firstOpts = me.resolveDataElementOptions(start, mode);
     const sharedOptions = me.getSharedOptions(firstOpts);
     const includeOptions = me.includeOptions(mode, sharedOptions);
-    const spanGaps = valueOrDefault(me._config.spanGaps, me.chart.options.spanGaps);
+    const spanGaps = me.options.spanGaps;
     const maxGapLength = isNumber(spanGaps) ? spanGaps : Number.POSITIVE_INFINITY;
     const directUpdate = me.chart._animationsDisabled || reset || mode === 'none';
     let prevParsed = start > 0 && me.getParsed(start - 1);
@@ -77,32 +79,6 @@ export default class LineController extends DatasetController {
     me.updateSharedOptions(sharedOptions, mode, firstOpts);
   }
 
-  /**
-        * @param {boolean} [active]
-        * @protected
-        */
-  resolveDatasetElementOptions(active) {
-    const me = this;
-    const config = me._config;
-    const options = me.chart.options;
-    const lineOptions = options.elements.line;
-    const values = super.resolveDatasetElementOptions(active);
-    const showLine = valueOrDefault(config.showLine, options.showLine);
-
-    // The default behavior of lines is to break at null values, according
-    // to https://github.com/chartjs/Chart.js/issues/2435#issuecomment-216718158
-    // This option gives lines the ability to span gaps
-    values.spanGaps = valueOrDefault(config.spanGaps, options.spanGaps);
-    values.tension = valueOrDefault(config.tension, lineOptions.tension);
-    values.stepped = resolve([config.stepped, lineOptions.stepped]);
-
-    if (!showLine) {
-      values.borderWidth = 0;
-    }
-
-    return values;
-  }
-
   /**
         * @protected
         */
@@ -132,37 +108,12 @@ LineController.id = 'line';
  */
 LineController.defaults = {
   datasetElementType: 'line',
-  datasetElementOptions: [
-    'backgroundColor',
-    'borderCapStyle',
-    'borderColor',
-    'borderDash',
-    'borderDashOffset',
-    'borderJoinStyle',
-    'borderWidth',
-    'capBezierPoints',
-    'cubicInterpolationMode',
-    'fill'
-  ],
-
   dataElementType: 'point',
-  dataElementOptions: {
-    backgroundColor: 'pointBackgroundColor',
-    borderColor: 'pointBorderColor',
-    borderWidth: 'pointBorderWidth',
-    hitRadius: 'pointHitRadius',
-    hoverHitRadius: 'pointHitRadius',
-    hoverBackgroundColor: 'pointHoverBackgroundColor',
-    hoverBorderColor: 'pointHoverBorderColor',
-    hoverBorderWidth: 'pointHoverBorderWidth',
-    hoverRadius: 'pointHoverRadius',
-    pointStyle: 'pointStyle',
-    radius: 'pointRadius',
-    rotation: 'pointRotation'
-  },
 
-  showLine: true,
-  spanGaps: false,
+  datasets: {
+    showLine: true,
+    spanGaps: false,
+  },
 
   interaction: {
     mode: 'index'
index 158590a18f0aacd9d180aabdca80736c1decc853..c88a3cbc3436b9e36760ca5e4bf683e6f443818c 100644 (file)
@@ -11,5 +11,14 @@ PieController.id = 'pie';
  * @type {any}
  */
 PieController.defaults = {
-  cutoutPercentage: 0
+  datasets: {
+    // The percentage of the chart that we cut out of the middle.
+    cutoutPercentage: 0,
+
+    // The rotation of the chart, where the first data arc begins.
+    rotation: 0,
+
+    // The total circumference of the chart.
+    circumference: 360
+  }
 };
index 0ce190395e2bd0ce8faa369456e51a056e6bcb32..a476c275a2454ef4114749966cb6dec34e410c4c 100644 (file)
@@ -1,5 +1,5 @@
 import DatasetController from '../core/core.datasetController';
-import {resolve, toRadians, PI} from '../helpers/index';
+import {toRadians, PI} from '../helpers/index';
 
 function getStartAngleRadians(deg) {
   // radialLinear scale draws angleLines using startAngle. 0 is expected to be at top.
@@ -55,16 +55,16 @@ export default class PolarAreaController extends DatasetController {
     let angle = datasetStartAngle;
     let i;
 
-    me._cachedMeta.count = me.countVisibleElements();
+    const defaultAngle = 360 / me.countVisibleElements();
 
     for (i = 0; i < start; ++i) {
-      angle += me._computeAngle(i, mode);
+      angle += me._computeAngle(i, mode, defaultAngle);
     }
     for (i = start; i < start + count; i++) {
       const arc = arcs[i];
       let startAngle = angle;
-      let endAngle = angle + me._computeAngle(i, mode);
-      let outerRadius = this.chart.getDataVisibility(i) ? scale.getDistanceFromCenterForValue(dataset.data[i]) : 0;
+      let endAngle = angle + me._computeAngle(i, mode, defaultAngle);
+      let outerRadius = chart.getDataVisibility(i) ? scale.getDistanceFromCenterForValue(dataset.data[i]) : 0;
       angle = endAngle;
 
       if (reset) {
@@ -72,8 +72,7 @@ export default class PolarAreaController extends DatasetController {
           outerRadius = 0;
         }
         if (animationOpts.animateRotate) {
-          startAngle = datasetStartAngle;
-          endAngle = datasetStartAngle;
+          startAngle = endAngle = datasetStartAngle;
         }
       }
 
@@ -108,23 +107,10 @@ export default class PolarAreaController extends DatasetController {
   /**
         * @private
         */
-  _computeAngle(index, mode) {
-    const me = this;
-    const meta = me._cachedMeta;
-    const count = meta.count;
-    const dataset = me.getDataset();
-
-    if (isNaN(dataset.data[index]) || !this.chart.getDataVisibility(index)) {
-      return 0;
-    }
-
-    // Scriptable options
-    const context = me.getContext(index, mode === 'active');
-
-    return toRadians(resolve([
-      me.chart.options.elements.arc.angle,
-      360 / count
-    ], context, index));
+  _computeAngle(index, mode, defaultAngle) {
+    return this.chart.getDataVisibility(index)
+      ? toRadians(this.resolveDataElementOptions(index, mode).angle || defaultAngle)
+      : 0;
   }
 }
 
@@ -135,14 +121,6 @@ PolarAreaController.id = 'polarArea';
  */
 PolarAreaController.defaults = {
   dataElementType: 'arc',
-  dataElementOptions: [
-    'backgroundColor',
-    'borderColor',
-    'borderWidth',
-    'borderAlign',
-    'offset'
-  ],
-
   animation: {
     numbers: {
       type: 'number',
index a2b8a2b12603777b2b5d241c5a06bb73dd0a992b..36473f600791546d766fcc60ac9c827c67990d20 100644 (file)
@@ -1,5 +1,4 @@
 import DatasetController from '../core/core.datasetController';
-import {valueOrDefault} from '../helpers/helpers.core';
 
 export default class RadarController extends DatasetController {
 
@@ -28,10 +27,15 @@ export default class RadarController extends DatasetController {
     line.points = points;
     // In resize mode only point locations change, so no need to set the points or options.
     if (mode !== 'resize') {
+      const options = me.resolveDatasetElementOptions(mode);
+      if (!me.options.showLine) {
+        options.borderWidth = 0;
+      }
+
       const properties = {
         _loop: true,
         _fullLoop: labels.length === points.length,
-        options: me.resolveDatasetElementOptions()
+        options
       };
 
       me.updateElement(line, undefined, properties, mode);
@@ -66,27 +70,6 @@ export default class RadarController extends DatasetController {
       me.updateElement(point, i, properties, mode);
     }
   }
-
-  /**
-        * @param {boolean} [active]
-        * @protected
-        */
-  resolveDatasetElementOptions(active) {
-    const me = this;
-    const config = me._config;
-    const options = me.chart.options;
-    const values = super.resolveDatasetElementOptions(active);
-    const showLine = valueOrDefault(config.showLine, options.showLine);
-
-    values.spanGaps = valueOrDefault(config.spanGaps, options.spanGaps);
-    values.tension = valueOrDefault(config.tension, options.elements.line.tension);
-
-    if (!showLine) {
-      values.borderWidth = 0;
-    }
-
-    return values;
-  }
 }
 
 RadarController.id = 'radar';
@@ -96,44 +79,20 @@ RadarController.id = 'radar';
  */
 RadarController.defaults = {
   datasetElementType: 'line',
-  datasetElementOptions: [
-    'backgroundColor',
-    'borderColor',
-    'borderCapStyle',
-    'borderDash',
-    'borderDashOffset',
-    'borderJoinStyle',
-    'borderWidth',
-    'fill'
-  ],
-
   dataElementType: 'point',
-  dataElementOptions: {
-    backgroundColor: 'pointBackgroundColor',
-    borderColor: 'pointBorderColor',
-    borderWidth: 'pointBorderWidth',
-    hitRadius: 'pointHitRadius',
-    hoverBackgroundColor: 'pointHoverBackgroundColor',
-    hoverBorderColor: 'pointHoverBorderColor',
-    hoverBorderWidth: 'pointHoverBorderWidth',
-    hoverRadius: 'pointHoverRadius',
-    pointStyle: 'pointStyle',
-    radius: 'pointRadius',
-    rotation: 'pointRotation'
-  },
-
   aspectRatio: 1,
-  spanGaps: false,
-  scales: {
-    r: {
-      type: 'radialLinear',
-    }
+  datasets: {
+    showLine: true,
   },
-  indexAxis: 'r',
   elements: {
     line: {
-      fill: 'start',
-      tension: 0 // no bezier in radar
+      fill: 'start'
+    }
+  },
+  indexAxis: 'r',
+  scales: {
+    r: {
+      type: 'radialLinear',
     }
   }
 };
index 285592db105a854791bc8821a59c777ca03c5181..1fc7a0e0d209f194e303692c11a5023a0456c3cc 100644 (file)
@@ -1,17 +1,18 @@
 import animator from './core.animator';
 import Animation from './core.animation';
 import defaults from './core.defaults';
-import {noop, isObject} from '../helpers/helpers.core';
+import {isObject} from '../helpers/helpers.core';
 
 const numbers = ['x', 'y', 'borderWidth', 'radius', 'tension'];
 const colors = ['borderColor', 'backgroundColor'];
+const animationOptions = ['duration', 'easing', 'from', 'to', 'type', 'easing', 'loop', 'fn'];
 
 defaults.set('animation', {
   // Plain properties can be overridden in each object
   duration: 1000,
   easing: 'easeOutQuart',
-  onProgress: noop,
-  onComplete: noop,
+  onProgress: undefined,
+  onComplete: undefined,
 
   // Property sets
   colors: {
@@ -54,30 +55,11 @@ defaults.set('animation', {
   }
 });
 
-function copyOptions(target, values) {
-  const oldOpts = target.options;
-  const newOpts = values.options;
-  if (!oldOpts || !newOpts) {
-    return;
-  }
-  if (oldOpts.$shared && !newOpts.$shared) {
-    target.options = Object.assign({}, oldOpts, newOpts, {$shared: false});
-  } else {
-    Object.assign(oldOpts, newOpts);
-  }
-  delete values.options;
-}
-
-function extensibleConfig(animations) {
-  const result = {};
-  Object.keys(animations).forEach(key => {
-    const value = animations[key];
-    if (!isObject(value)) {
-      result[key] = value;
-    }
-  });
-  return result;
-}
+defaults.describe('animation', {
+  _scriptable: (name) => name !== 'onProgress' && name !== 'onComplete' && name !== 'fn',
+  _indexable: false,
+  _fallback: 'animation',
+});
 
 export default class Animations {
   constructor(chart, animations) {
@@ -92,22 +74,20 @@ export default class Animations {
     }
 
     const animatedProps = this._properties;
-    const animDefaults = extensibleConfig(animations);
 
-    Object.keys(animations).forEach(key => {
+    Object.getOwnPropertyNames(animations).forEach(key => {
       const cfg = animations[key];
       if (!isObject(cfg)) {
         return;
       }
+      const resolved = {};
+      for (const option of animationOptions) {
+        resolved[option] = cfg[option];
+      }
+
       (cfg.properties || [key]).forEach((prop) => {
-        // Can have only one config per animation.
-        if (!animatedProps.has(prop)) {
-          animatedProps.set(prop, Object.assign({}, animDefaults, cfg));
-        } else if (prop === key) {
-          // Single property targetting config wins over multi-targetting.
-          // eslint-disable-next-line no-unused-vars
-          const {properties, ...inherited} = animatedProps.get(prop);
-          animatedProps.set(prop, Object.assign({}, inherited, cfg));
+        if (prop === key || !animatedProps.has(prop)) {
+          animatedProps.set(prop, resolved);
         }
       });
     });
@@ -125,8 +105,8 @@ export default class Animations {
     }
 
     const animations = this._createAnimations(options, newOptions);
-    if (newOptions.$shared && !options.$shared) {
-      // Going from distinct options to shared options:
+    if (newOptions.$shared) {
+      // Going to shared options:
       // After all animations are done, assign the shared options object to the element
       // So any new updates to the shared options are observed
       awaitAll(target.options.$animations, newOptions).then(() => {
@@ -195,10 +175,6 @@ export default class Animations {
   update(target, values) {
     if (this._properties.size === 0) {
       // Nothing is animated, just apply the new values.
-      // Options can be shared, need to account for that.
-      copyOptions(target, values);
-      // copyOptions removes the `options` from `values`,
-      // unless it can be directly assigned.
       Object.assign(target, values);
       return;
     }
@@ -234,7 +210,7 @@ function resolveTargetOptions(target, newOptions) {
     target.options = newOptions;
     return;
   }
-  if (options.$shared && !newOptions.$shared) {
+  if (options.$shared) {
     // Going from shared options to distinct one:
     // Create new options object containing the old shared values and start updating that.
     target.options = options = Object.assign({}, options, {$shared: false, $animations: {}});
index 06fcbbefff9847c273f78a108f59a686eafb1b40..a8798965ccce32eb0418e15a03f7c990c437f649 100644 (file)
@@ -1,5 +1,6 @@
 import defaults from './core.defaults';
-import {mergeIf, merge, _merger} from '../helpers/helpers.core';
+import {mergeIf, resolveObjectKey, isArray, isFunction, valueOrDefault} from '../helpers/helpers.core';
+import {_attachContext, _createResolver, _descriptors} from '../helpers/helpers.config';
 
 export function getIndexAxis(type, options) {
   const typeDefaults = defaults.controllers[type] || {};
@@ -79,59 +80,12 @@ function mergeScaleConfig(config, options) {
   return scales;
 }
 
-/**
- * Recursively merge the given config objects as the root options by handling
- * default scale options for the `scales` and `scale` properties, then returns
- * a deep copy of the result, thus doesn't alter inputs.
- */
-function mergeConfig(...args/* config objects ... */) {
-  return merge(Object.create(null), args, {
-    merger(key, target, source, options) {
-      if (key !== 'scales' && key !== 'scale' && key !== 'controllers') {
-        _merger(key, target, source, options);
-      }
-    }
-  });
-}
-
-function includePluginDefaults(options) {
-  options.plugins = options.plugins || {};
-  options.plugins.title = (options.plugins.title !== false) && merge(Object.create(null), [
-    defaults.plugins.title,
-    options.plugins.title
-  ]);
-
-  options.plugins.tooltip = (options.plugins.tooltip !== false) && merge(Object.create(null), [
-    defaults.interaction,
-    defaults.plugins.tooltip,
-    options.interaction,
-    options.plugins.tooltip
-  ]);
-}
-
-function includeDefaults(config, options) {
+function initOptions(config, options) {
   options = options || {};
 
-  const scaleConfig = mergeScaleConfig(config, options);
-  const hoverEanbled = options.interaction !== false && options.hover !== false;
-
-  options = mergeConfig(
-    defaults,
-    defaults.controllers[config.type],
-    options);
-
-  options.hover = hoverEanbled && merge(Object.create(null), [
-    defaults.interaction,
-    defaults.hover,
-    options.interaction,
-    options.hover
-  ]);
-
-  options.scales = scaleConfig;
+  options.plugins = valueOrDefault(options.plugins, {});
+  options.scales = mergeScaleConfig(config, options);
 
-  if (options.plugins !== false) {
-    includePluginDefaults(options);
-  }
   return options;
 }
 
@@ -144,7 +98,7 @@ function initConfig(config) {
   data.datasets = data.datasets || [];
   data.labels = data.labels || [];
 
-  config.options = includeDefaults(config, config.options);
+  config.options = initOptions(config, config.options);
 
   return config;
 }
@@ -180,6 +134,135 @@ export default class Config {
 
   update(options) {
     const config = this._config;
-    config.options = includeDefaults(config, options);
+    config.options = initOptions(config, options);
+  }
+
+  /**
+        * 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 [`datasets.${datasetType}`, `controllers.${datasetType}.datasets`, ''];
+  }
+
+  /**
+        * 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 [`datasets.${datasetType}.animation`, `controllers.${datasetType}.datasets.animation`, 'animation'];
+  }
+
+  /**
+        * 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 [
+      `datasets.${datasetType}`,
+      `controllers.${datasetType}.datasets`,
+      `controllers.${datasetType}.elements.${elementType}`,
+      `elements.${elementType}`,
+      ''
+    ];
+  }
+
+  /**
+        * 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
+        */
+  getOptionScopes(mainScope = {}, scopeKeys) {
+    const options = this.options;
+    const scopes = new Set([mainScope]);
+
+    const addIfFound = (obj, key) => {
+      const opts = resolveObjectKey(obj, key);
+      if (opts !== undefined) {
+        scopes.add(opts);
+      }
+    };
+
+    scopeKeys.forEach(key => addIfFound(mainScope, key));
+    scopeKeys.forEach(key => addIfFound(options, key));
+    scopeKeys.forEach(key => addIfFound(defaults, key));
+
+    const descriptors = defaults.descriptors;
+    scopeKeys.forEach(key => addIfFound(descriptors, key));
+
+    return [...scopes];
+  }
+
+  /**
+        * Returns the option scopes for resolving chart options
+        * @return {object[]}
+        */
+  chartOptionsScopes() {
+    return [
+      this.options,
+      defaults.controllers[this.type] || {},
+      {type: this.type},
+      defaults, defaults.descriptors
+    ];
+  }
+
+  /**
+        * @param {object[]} scopes
+        * @param {string[]} names
+        * @param {function|object} context
+        * @param {string[]} [prefixes]
+        * @return {object}
+        */
+  resolveNamedOptions(scopes, names, context, prefixes = ['']) {
+    const result = {};
+    const resolver = _createResolver(scopes, prefixes);
+    let options;
+    if (needContext(resolver, names)) {
+      result.$shared = false;
+      context = isFunction(context) ? context() : context;
+      // subResolver os passed to scriptable options. It should not resolve to hover options.
+      const subPrefixes = prefixes.filter(p => !p.toLowerCase().includes('hover'));
+      const subResolver = this.createResolver(scopes, context, subPrefixes);
+      options = _attachContext(resolver, context, subResolver);
+    } else {
+      result.$shared = true;
+      options = resolver;
+    }
+
+    for (const prop of names) {
+      result[prop] = options[prop];
+    }
+    return result;
+  }
+
+  /**
+        * @param {object[]} scopes
+        * @param {function|object} context
+        */
+  createResolver(scopes, context, prefixes = ['']) {
+    const resolver = _createResolver(scopes, prefixes);
+    return context && needContext(resolver, Object.getOwnPropertyNames(resolver))
+      ? _attachContext(resolver, isFunction(context) ? context() : context)
+      : resolver;
+  }
+}
+
+function needContext(proxy, names) {
+  const {isScriptable, isIndexable} = _descriptors(proxy);
+
+  for (const prop of names) {
+    if ((isScriptable(prop) && isFunction(proxy[prop]))
+                       || (isIndexable(prop) && isArray(proxy[prop]))) {
+      return true;
+    }
   }
+  return false;
 }
index fce289cf0c534e2569e272a55a5a63b31fb6983d..12c62afd4719d4f73522c822812bae843afaef94 100644 (file)
@@ -82,9 +82,11 @@ class Chart {
       );
     }
 
+    const options = config.createResolver(config.chartOptionsScopes(), me.getContext());
+
     this.platform = me._initializePlatform(initialCanvas, config);
 
-    const context = me.platform.acquireContext(initialCanvas, config);
+    const context = me.platform.acquireContext(initialCanvas, options.aspectRatio);
     const canvas = context && context.canvas;
     const height = canvas && canvas.height;
     const width = canvas && canvas.width;
@@ -95,7 +97,7 @@ class Chart {
     this.width = width;
     this.height = height;
     this.aspectRatio = height ? width / height : null;
-    this.options = config.options;
+    this._options = options;
     this._layers = [];
     this._metasets = [];
     this.boxes = [];
@@ -144,6 +146,14 @@ class Chart {
     this.config.data = data;
   }
 
+  get options() {
+    return this._options;
+  }
+
+  set options(options) {
+    this.config.update(options);
+  }
+
   /**
         * @private
         */
@@ -394,9 +404,7 @@ class Chart {
         const ControllerClass = registry.getController(type);
         Object.assign(ControllerClass.prototype, {
           dataElementType: registry.getElement(controllerDefaults.dataElementType),
-          datasetElementType: controllerDefaults.datasetElementType && registry.getElement(controllerDefaults.datasetElementType),
-          dataElementOptions: controllerDefaults.dataElementOptions,
-          datasetElementOptions: controllerDefaults.datasetElementOptions
+          datasetElementType: controllerDefaults.datasetElementType && registry.getElement(controllerDefaults.datasetElementType)
         });
         meta.controller = new ControllerClass(me, i);
         newControllers.push(meta.controller);
@@ -428,13 +436,15 @@ class Chart {
 
   update(mode) {
     const me = this;
+    const config = me.config;
+
+    config.update(config.options);
+    me._options = config.createResolver(config.chartOptionsScopes(), me.getContext());
 
     each(me.scales, (scale) => {
       layouts.removeBox(me, scale);
     });
 
-    me.config.update(me.options);
-    me.options = me.config.options;
     const animsDisabled = me._animationsDisabled = !me.options.animation;
 
     me.ensureScalesHaveIDs();
@@ -985,8 +995,7 @@ class Chart {
         */
   _updateHoverStyles(active, lastActive, replay) {
     const me = this;
-    const options = me.options || {};
-    const hoverOptions = options.hover;
+    const hoverOptions = me.options.hover;
     const diff = (a, b) => a.filter(x => !b.some(y => x.datasetIndex === y.datasetIndex && x.index === y.index));
     const deactivated = diff(lastActive, active);
     const activated = replay ? active : diff(active, lastActive);
index a06946b1f029a5ad99f8ae74b8d61172aed8ca96..9243352961cca31c51784dd8d444b7f886c2c4da 100644 (file)
@@ -1,9 +1,7 @@
 import Animations from './core.animations';
 import defaults from './core.defaults';
-import {isObject, merge, _merger, isArray, valueOrDefault, mergeIf, resolveObjectKey, _capitalize} from '../helpers/helpers.core';
+import {isObject, isArray, valueOrDefault, resolveObjectKey, defined} from '../helpers/helpers.core';
 import {listenArrayEvents, unlistenArrayEvents} from '../helpers/helpers.collection';
-import {resolve} from '../helpers/helpers.options';
-import {getHoverColor} from '../helpers/helpers.color';
 import {sign} from '../helpers/helpers.math';
 
 /**
@@ -183,8 +181,6 @@ function clearStacks(meta, items) {
   });
 }
 
-const optionKeys = (optionNames) => isArray(optionNames) ? optionNames : Object.keys(optionNames);
-const optionKey = (key, active) => active ? 'hover' + _capitalize(key) : key;
 const isDirectUpdateMode = (mode) => mode === 'reset' || mode === 'none';
 const cloneIfNotShared = (cached, shared) => shared ? cached : Object.assign({}, cached);
 
@@ -198,11 +194,10 @@ export default class DatasetController {
     this.chart = chart;
     this._ctx = chart.ctx;
     this.index = datasetIndex;
-    this._cachedAnimations = {};
     this._cachedDataOpts = {};
     this._cachedMeta = this.getMeta();
     this._type = this._cachedMeta.type;
-    this._config = undefined;
+    this.options = undefined;
     /** @type {boolean | object} */
     this._parsing = false;
     this._data = undefined;
@@ -365,21 +360,11 @@ export default class DatasetController {
         */
   configure() {
     const me = this;
-    me._config = merge(Object.create(null), [
-      defaults.controllers[me._type].datasets,
-      (me.chart.options.datasets || {})[me._type],
-      me.getDataset(),
-    ], {
-      merger(key, target, source) {
-        // Cloning the data is expensive and unnecessary.
-        // Additionally, plugins may add dataset level fields that should
-        // not be cloned. We identify those via an underscore prefix
-        if (key !== 'data' && key.charAt(0) !== '_') {
-          _merger(key, target, source);
-        }
-      }
-    });
-    me._parsing = resolve([me._config.parsing, me.chart.options.parsing, true]);
+    const config = me.chart.config;
+    const scopeKeys = config.datasetScopeKeys(me._type);
+    const scopes = config.getOptionScopes(me.getDataset(), scopeKeys);
+    me.options = config.createResolver(scopes, me.getContext());
+    me._parsing = me.options.parsing;
   }
 
   /**
@@ -646,10 +631,9 @@ export default class DatasetController {
     const me = this;
     const meta = me._cachedMeta;
     me.configure();
-    me._cachedAnimations = {};
     me._cachedDataOpts = {};
     me.update(mode || 'default');
-    meta._clip = toClip(valueOrDefault(me._config.clip, defaultClip(meta.xScale, meta.yScale, me.getMaxOverflow())));
+    meta._clip = toClip(valueOrDefault(me.options.clip, defaultClip(meta.xScale, meta.yScale, me.getMaxOverflow())));
   }
 
   /**
@@ -687,21 +671,6 @@ export default class DatasetController {
     }
   }
 
-  /**
-        * @private
-        */
-  _addAutomaticHoverColors(index, options) {
-    const me = this;
-    const normalOptions = me.getStyle(index);
-    const missingColors = Object.keys(normalOptions).filter(key => key.indexOf('Color') !== -1 && !(key in options));
-    let i = missingColors.length - 1;
-    let color;
-    for (; i >= 0; i--) {
-      color = missingColors[i];
-      options[color] = getHoverColor(normalOptions[color]);
-    }
-  }
-
   /**
         * Returns a set of predefined style properties that should be used to represent the dataset
         * or the data if the index is specified
@@ -710,28 +679,16 @@ export default class DatasetController {
         * @return {object} style object
         */
   getStyle(index, active) {
-    const me = this;
-    const meta = me._cachedMeta;
-    const dataset = meta.dataset;
-
-    if (!me._config) {
-      me.configure();
-    }
-
-    const options = dataset && index === undefined
-      ? me.resolveDatasetElementOptions(active)
-      : me.resolveDataElementOptions(index || 0, active && 'active');
-    if (active) {
-      me._addAutomaticHoverColors(index, options);
-    }
-
-    return options;
+    const mode = active ? 'active' : 'default';
+    return index === undefined && this._cachedMeta.dataset
+      ? this.resolveDatasetElementOptions(mode)
+      : this.resolveDataElementOptions(index || 0, mode);
   }
 
   /**
         * @protected
         */
-  getContext(index, active) {
+  getContext(index, active, mode) {
     const me = this;
     const dataset = me.getDataset();
     let context;
@@ -744,18 +701,16 @@ export default class DatasetController {
     }
 
     context.active = !!active;
+    context.mode = mode;
     return context;
   }
 
   /**
-        * @param {boolean} [active]
+        * @param {string} [mode]
         * @protected
         */
-  resolveDatasetElementOptions(active) {
-    return this._resolveOptions(this.datasetElementOptions, {
-      active,
-      type: this.datasetElementType.id
-    });
+  resolveDatasetElementOptions(mode) {
+    return this._resolveElementOptions(this.datasetElementType.id, mode);
   }
 
   /**
@@ -764,25 +719,33 @@ export default class DatasetController {
         * @protected
         */
   resolveDataElementOptions(index, mode) {
-    mode = mode || 'default';
+    return this._resolveElementOptions(this.dataElementType.id, mode, index);
+  }
+
+  /**
+        * @private
+        */
+  _resolveElementOptions(elementType, mode = 'default', index) {
     const me = this;
     const active = mode === 'active';
     const cache = me._cachedDataOpts;
-    const cached = cache[mode];
-    const sharing = me.enableOptionSharing;
+    const cacheKey = elementType + '-' + mode;
+    const cached = cache[cacheKey];
+    const sharing = me.enableOptionSharing && defined(index);
     if (cached) {
       return cloneIfNotShared(cached, sharing);
     }
-    const info = {cacheable: !active};
-
-    const values = me._resolveOptions(me.dataElementOptions, {
-      index,
-      active,
-      info,
-      type: me.dataElementType.id
-    });
-
-    if (info.cacheable) {
+    const config = me.chart.config;
+    const scopeKeys = config.datasetElementScopeKeys(me._type, elementType);
+    const prefixes = active ? [`${elementType}Hover`, 'hover', elementType, ''] : [elementType, ''];
+    const scopes = config.getOptionScopes(me.getDataset(), scopeKeys);
+    const names = Object.keys(defaults.elements[elementType]);
+    // context is provided as a function, and is called only if needed,
+    // so we don't create a context for each element if not needed.
+    const context = () => me.getContext(index, active);
+    const values = config.resolveNamedOptions(scopes, names, context, prefixes);
+
+    if (values.$shared) {
       // `$shared` indicates this set of options can be shared between multiple elements.
       // Sharing is used to reduce number of properties to change during animation.
       values.$shared = sharing;
@@ -790,41 +753,12 @@ export default class DatasetController {
       // We cache options by `mode`, which can be 'active' for example. This enables us
       // to have the 'active' element options and 'default' options to switch between
       // when interacting.
-      // We freeze a clone of this object, so the returned values are not frozen.
-      cache[mode] = Object.freeze(Object.assign({}, values));
+      cache[cacheKey] = Object.freeze(cloneIfNotShared(values, sharing));
     }
 
     return values;
   }
 
-  /**
-        * @private
-        */
-  _resolveOptions(optionNames, args) {
-    const me = this;
-    const {index, active, type, info} = args;
-    const datasetOpts = me._config;
-    const options = me.chart.options.elements[type] || {};
-    const values = {};
-    const context = me.getContext(index, active);
-    const keys = optionKeys(optionNames);
-
-    for (let i = 0, ilen = keys.length; i < ilen; ++i) {
-      const key = keys[i];
-      const readKey = optionKey(key, active);
-      const value = resolve([
-        datasetOpts[optionNames[readKey]],
-        datasetOpts[readKey],
-        options[readKey]
-      ], context, index, info);
-
-      if (value !== undefined) {
-        values[key] = value;
-      }
-    }
-
-    return values;
-  }
 
   /**
         * @private
@@ -832,29 +766,24 @@ export default class DatasetController {
   _resolveAnimations(index, mode, active) {
     const me = this;
     const chart = me.chart;
-    const cached = me._cachedAnimations;
-    mode = mode || 'default';
-
-    if (cached[mode]) {
-      return cached[mode];
+    const cache = me._cachedDataOpts;
+    const cacheKey = 'animation-' + mode;
+    const cached = cache[cacheKey];
+    if (cached) {
+      return cached;
     }
-
-    const info = {cacheable: true};
-    const context = me.getContext(index, active);
-    const chartAnim = resolve([chart.options.animation], context, index, info);
-    const datasetAnim = resolve([me._config.animation], context, index, info);
-    let config = chartAnim && mergeIf({}, [datasetAnim, chartAnim]);
-
-    if (config[mode]) {
-      config = Object.assign({}, config, config[mode]);
+    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);
+      const context = () => me.getContext(index, active, mode);
+      options = config.createResolver(scopes, context);
     }
-
-    const animations = new Animations(chart, config);
-
-    if (info.cacheable) {
-      cached[mode] = animations && Object.freeze(animations);
+    const animations = new Animations(chart, options && options[mode] || options);
+    if (options && options._cacheable) {
+      cache[cacheKey] = Object.freeze(animations);
     }
-
     return animations;
   }
 
@@ -895,7 +824,7 @@ export default class DatasetController {
         */
   updateSharedOptions(sharedOptions, mode, newOptions) {
     if (sharedOptions) {
-      this._resolveAnimations(undefined, mode).update({options: sharedOptions}, {options: newOptions});
+      this._resolveAnimations(undefined, mode).update(sharedOptions, newOptions);
     }
   }
 
@@ -905,7 +834,11 @@ export default class DatasetController {
   _setStyle(element, index, mode, active) {
     element.active = active;
     const options = this.getStyle(index, active);
-    this._resolveAnimations(index, mode, active).update(element, {options: this.getSharedOptions(options) || options});
+    this._resolveAnimations(index, mode, active).update(element, {
+      // When going from active to inactive, we need to update to the shared options.
+      // This way the once hovered element will end up with the same original shared options instance, after animation.
+      options: (!active && this.getSharedOptions(options)) || options
+    });
   }
 
   removeHoverStyle(element, datasetIndex, index) {
@@ -1060,32 +993,3 @@ DatasetController.prototype.datasetElementType = null;
  * Element type used to generate a meta data (e.g. Chart.element.PointElement).
  */
 DatasetController.prototype.dataElementType = null;
-
-/**
- * Dataset element option keys to be resolved in resolveDatasetElementOptions.
- * A derived controller may override this to resolve controller-specific options.
- * The keys defined here are for backward compatibility for legend styles.
- * @type {string[]}
- */
-DatasetController.prototype.datasetElementOptions = [
-  'backgroundColor',
-  'borderCapStyle',
-  'borderColor',
-  'borderDash',
-  'borderDashOffset',
-  'borderJoinStyle',
-  'borderWidth'
-];
-
-/**
- * Data element option keys to be resolved in resolveDataElementOptions.
- * A derived controller may override this to resolve controller-specific options.
- * The keys defined here are for backward compatibility for legend styles.
- * @type {string[]|object}
- */
-DatasetController.prototype.dataElementOptions = [
-  'backgroundColor',
-  'borderColor',
-  'borderWidth',
-  'pointStyle'
-];
index 14b16e1c4f094687f2cb68e2ad0be78e17763179..94dc1a9ef5f4cf862c43d02535fa0c61dd0eed92 100644 (file)
@@ -1,5 +1,8 @@
+import {getHoverColor} from '../helpers/helpers.color';
 import {isObject, merge, valueOrDefault} from '../helpers/helpers.core';
 
+const privateSymbol = Symbol('private');
+
 /**
  * @param {object} node
  * @param {string} key
@@ -22,11 +25,13 @@ function getScope(node, key) {
  * Note: class is exported for typedoc
  */
 export class Defaults {
-  constructor() {
+  constructor(descriptors) {
+    this.animation = undefined;
     this.backgroundColor = 'rgba(0,0,0,0.1)';
     this.borderColor = 'rgba(0,0,0,0.1)';
     this.color = '#666';
     this.controllers = {};
+    this.devicePixelRatio = (context) => context.chart.platform.getDevicePixelRatio();
     this.elements = {};
     this.events = [
       'mousemove',
@@ -45,6 +50,10 @@ export class Defaults {
     this.hover = {
       onHover: null
     };
+    this.hoverBackgroundColor = (ctx, options) => getHoverColor(options.backgroundColor);
+    this.hoverBorderColor = (ctx, options) => getHoverColor(options.borderColor);
+    this.hoverColor = (ctx, options) => getHoverColor(options.color);
+    this.indexAxis = 'x';
     this.interaction = {
       mode: 'nearest',
       intersect: true
@@ -52,11 +61,19 @@ export class Defaults {
     this.maintainAspectRatio = true;
     this.onHover = null;
     this.onClick = null;
+    this.parsing = true;
     this.plugins = {};
     this.responsive = true;
     this.scale = undefined;
     this.scales = {};
     this.showLine = true;
+
+    Object.defineProperty(this, privateSymbol, {
+      value: Object.create(null),
+      writable: false
+    });
+
+    this.describe(descriptors);
   }
 
   /**
@@ -77,6 +94,22 @@ export class Defaults {
     return getScope(this, scope);
   }
 
+  /**
+        * @param {string|object} scope
+        * @param {object} [values]
+        */
+  describe(scope, values) {
+    const root = this[privateSymbol];
+    if (typeof scope === 'string') {
+      return merge(getScope(root, scope), values);
+    }
+    return merge(getScope(root, ''), scope);
+  }
+
+  get descriptors() {
+    return this[privateSymbol];
+  }
+
   /**
         * Routes the named defaults to fallback to another scope/name.
         * This routing is useful when those target values, like defaults.color, are changed runtime.
@@ -125,4 +158,14 @@ export class Defaults {
 }
 
 // singleton instance
-export default new Defaults();
+export default new Defaults({
+  _scriptable: (name) => name !== 'onClick' && name !== 'onHover',
+  _indexable: (name) => name !== 'events',
+  hover: {
+    _fallback: 'interaction'
+  },
+  interaction: {
+    _scriptable: false,
+    _indexable: false,
+  }
+});
index 31a4cb618a1cbb53c46bf53cddf8edd17f3ca53c..30661a17f0da8c4cf2746249bd1587c01ece8323 100644 (file)
@@ -1,6 +1,6 @@
 import defaults from './core.defaults';
 import {each, isObject} from '../helpers/helpers.core';
-import {toPadding, resolve} from '../helpers/helpers.options';
+import {toPadding} from '../helpers/helpers.options';
 
 /**
  * @typedef { import("./core.controller").default } Chart
@@ -301,10 +301,7 @@ export default {
       return;
     }
 
-    const layoutOptions = chart.options.layout || {};
-    const context = {chart};
-    const padding = toPadding(resolve([layoutOptions.padding], context));
-
+    const padding = toPadding(chart.options.layout.padding);
     const availableWidth = width - padding.width;
     const availableHeight = height - padding.height;
     const boxes = buildLayoutBoxes(chart.boxes);
index 42f962c5b471b6c7b993ea3d8d9822a8e7916939..847ba4a765c7a41a306ca88f21b2808926869c7f 100644 (file)
@@ -1,7 +1,5 @@
-import defaults from './core.defaults';
 import registry from './core.registry';
-import {isNullOrUndef} from '../helpers';
-import {callback as callCallback, mergeIf, valueOrDefault} from '../helpers/helpers.core';
+import {callback as callCallback, isNullOrUndef, valueOrDefault} from '../helpers/helpers.core';
 
 /**
  * @typedef { import("./core.controller").default } Chart
@@ -91,7 +89,7 @@ export default class PluginService {
     const options = valueOrDefault(config.options && config.options.plugins, {});
     const plugins = allPlugins(config);
     // options === false => all plugins are disabled
-    return options === false && !all ? [] : createDescriptors(plugins, options, all);
+    return options === false && !all ? [] : createDescriptors(chart, plugins, options, all);
   }
 
   /**
@@ -139,8 +137,9 @@ function getOpts(options, all) {
   return options;
 }
 
-function createDescriptors(plugins, options, all) {
+function createDescriptors(chart, plugins, options, all) {
   const result = [];
+  const context = chart.getContext();
 
   for (let i = 0; i < plugins.length; i++) {
     const plugin = plugins[i];
@@ -151,9 +150,26 @@ function createDescriptors(plugins, options, all) {
     }
     result.push({
       plugin,
-      options: mergeIf({}, [opts, defaults.plugins[id]])
+      options: pluginOpts(chart.config, plugin, opts, context)
     });
   }
 
   return result;
 }
+
+/**
+ * @param {import("./core.config").default} config
+ * @param {*} plugin
+ * @param {*} opts
+ * @param {*} context
+ */
+function pluginOpts(config, plugin, opts, context) {
+  const id = plugin.id;
+  const keys = [
+    `controllers.${config.type}.plugins.${id}`,
+    `plugins.${id}`,
+    ...plugin.additionalOptionScopes || []
+  ];
+  const scopes = config.getOptionScopes(opts || {}, keys);
+  return config.createResolver(scopes, context);
+}
index 00eff1fd292545fd7881026bd44b58797d487e48..26181bb9cf5bd37c5835c744d79379a334fa8d74 100644 (file)
@@ -3,7 +3,7 @@ import Element from './core.element';
 import {_alignPixel, _measureText, renderText, clipArea, unclipArea} from '../helpers/helpers.canvas';
 import {callback as call, each, finiteOrDefault, isArray, isFinite, isNullOrUndef, isObject, valueOrDefault} from '../helpers/helpers.core';
 import {_factorize, toDegrees, toRadians, _int16Range, HALF_PI} from '../helpers/helpers.math';
-import {toFont, resolve, toPadding} from '../helpers/helpers.options';
+import {toFont, toPadding} from '../helpers/helpers.options';
 import Ticks from './core.ticks';
 
 /**
@@ -34,9 +34,13 @@ defaults.set('scale', {
     drawOnChartArea: true,
     drawTicks: true,
     tickLength: 10,
+    tickWidth: (_ctx, options) => options.lineWidth,
+    tickColor: (_ctx, options) => options.color,
     offsetGridLines: false,
     borderDash: [],
-    borderDashOffset: 0.0
+    borderDashOffset: 0.0,
+    borderColor: (_ctx, options) => options.color,
+    borderWidth: (_ctx, options) => options.lineWidth
   },
 
   // scale label
@@ -79,6 +83,12 @@ defaults.route('scale.ticks', 'color', '', 'color');
 defaults.route('scale.gridLines', 'color', '', 'borderColor');
 defaults.route('scale.scaleLabel', 'color', '', 'color');
 
+defaults.describe('scales', {
+  _fallback: 'scale',
+  _scriptable: (name) => !name.startsWith('before') && !name.startsWith('after') && name !== 'callback' && name !== 'parser',
+  _indexable: (name) => name !== 'borderDash' && name !== 'tickBorderDash',
+});
+
 /**
  * Returns a new array containing numItems from arr
  * @param {any[]} arr
@@ -386,7 +396,7 @@ export default class Scale extends Element {
     const me = this;
     me.options = options;
 
-    me.axis = me.isHorizontal() ? 'x' : 'y';
+    me.axis = options.axis;
 
     // parse min/max value, so we can properly determine min/max for other scales
     me._userMin = me.parse(options.min);
@@ -1196,8 +1206,8 @@ export default class Scale extends Element {
     const tl = getTickMarkLength(gridLines);
     const items = [];
 
-    let context = this.getContext(0);
-    const axisWidth = gridLines.drawBorder ? resolve([gridLines.borderWidth, gridLines.lineWidth, 0], context, 0) : 0;
+    const borderOpts = gridLines.setContext(me.getContext(0));
+    const axisWidth = borderOpts.drawBorder ? borderOpts.borderWidth : 0;
     const axisHalfWidth = axisWidth / 2;
     const alignBorderValue = function(pixel) {
       return _alignPixel(chart, pixel, axisWidth);
@@ -1258,17 +1268,17 @@ export default class Scale extends Element {
     }
 
     for (i = 0; i < ticksLength; ++i) {
-      context = this.getContext(i);
+      const optsAtIndex = gridLines.setContext(me.getContext(i));
 
-      const lineWidth = resolve([gridLines.lineWidth], context, i);
-      const lineColor = resolve([gridLines.color], context, i);
+      const lineWidth = optsAtIndex.lineWidth;
+      const lineColor = optsAtIndex.color;
       const borderDash = gridLines.borderDash || [];
-      const borderDashOffset = resolve([gridLines.borderDashOffset], context, i);
+      const borderDashOffset = optsAtIndex.borderDashOffset;
 
-      const tickWidth = resolve([gridLines.tickWidth, lineWidth], context, i);
-      const tickColor = resolve([gridLines.tickColor, lineColor], context, i);
-      const tickBorderDash = gridLines.tickBorderDash || borderDash;
-      const tickBorderDashOffset = resolve([gridLines.tickBorderDashOffset, borderDashOffset], context, i);
+      const tickWidth = optsAtIndex.tickWidth;
+      const tickColor = optsAtIndex.tickColor;
+      const tickBorderDash = optsAtIndex.tickBorderDash || [];
+      const tickBorderDashOffset = optsAtIndex.tickBorderDashOffset;
 
       lineValue = getPixelForGridLine(me, i, offsetGridLines);
 
@@ -1376,14 +1386,15 @@ export default class Scale extends Element {
       tick = ticks[i];
       label = tick.label;
 
+      const optsAtIndex = optionTicks.setContext(me.getContext(i));
       pixel = me.getPixelForTick(i) + optionTicks.labelOffset;
       font = me._resolveTickFontOptions(i);
       lineHeight = font.lineHeight;
       lineCount = isArray(label) ? label.length : 1;
       const halfCount = lineCount / 2;
-      const color = resolve([optionTicks.color], me.getContext(i), i);
-      const strokeColor = resolve([optionTicks.textStrokeColor], me.getContext(i), i);
-      const strokeWidth = resolve([optionTicks.textStrokeWidth], me.getContext(i), i);
+      const color = optsAtIndex.color;
+      const strokeColor = optsAtIndex.textStrokeColor;
+      const strokeWidth = optsAtIndex.textStrokeWidth;
 
       if (isHorizontal) {
         x = pixel;
@@ -1529,8 +1540,8 @@ export default class Scale extends Element {
     const gridLines = me.options.gridLines;
     const ctx = me.ctx;
     const chart = me.chart;
-    let context = me.getContext(0);
-    const axisWidth = gridLines.drawBorder ? resolve([gridLines.borderWidth, gridLines.lineWidth, 0], context, 0) : 0;
+    const borderOpts = gridLines.setContext(me.getContext(0));
+    const axisWidth = gridLines.drawBorder ? borderOpts.borderWidth : 0;
     const items = me._gridLineItems || (me._gridLineItems = me._computeGridLineItems(chartArea));
     let i, ilen;
 
@@ -1575,24 +1586,23 @@ export default class Scale extends Element {
 
     if (axisWidth) {
       // Draw the line at the edge of the axis
-      const firstLineWidth = axisWidth;
-      context = me.getContext(me._ticksLength - 1);
-      const lastLineWidth = resolve([gridLines.lineWidth, 1], context, me._ticksLength - 1);
+      const edgeOpts = gridLines.setContext(me.getContext(me._ticksLength - 1));
+      const lastLineWidth = edgeOpts.lineWidth;
       const borderValue = me._borderValue;
       let x1, x2, y1, y2;
 
       if (me.isHorizontal()) {
-        x1 = _alignPixel(chart, me.left, firstLineWidth) - firstLineWidth / 2;
+        x1 = _alignPixel(chart, me.left, axisWidth) - axisWidth / 2;
         x2 = _alignPixel(chart, me.right, lastLineWidth) + lastLineWidth / 2;
         y1 = y2 = borderValue;
       } else {
-        y1 = _alignPixel(chart, me.top, firstLineWidth) - firstLineWidth / 2;
+        y1 = _alignPixel(chart, me.top, axisWidth) - axisWidth / 2;
         y2 = _alignPixel(chart, me.bottom, lastLineWidth) + lastLineWidth / 2;
         x1 = x2 = borderValue;
       }
 
       ctx.lineWidth = axisWidth;
-      ctx.strokeStyle = resolve([gridLines.borderColor, gridLines.color], context, 0);
+      ctx.strokeStyle = edgeOpts.borderColor;
       ctx.beginPath();
       ctx.moveTo(x1, y1);
       ctx.lineTo(x2, y2);
@@ -1647,7 +1657,7 @@ export default class Scale extends Element {
       return;
     }
 
-    const scaleLabelFont = toFont(scaleLabel.font, me.chart.options.font);
+    const scaleLabelFont = toFont(scaleLabel.font);
     const scaleLabelPadding = toPadding(scaleLabel.padding);
     const halfLineHeight = scaleLabelFont.lineHeight / 2;
     const scaleLabelAlign = scaleLabel.align;
@@ -1778,11 +1788,8 @@ export default class Scale extends Element {
         * @protected
         */
   _resolveTickFontOptions(index) {
-    const me = this;
-    const chart = me.chart;
-    const options = me.options.ticks;
-    const context = me.getContext(index);
-    return toFont(resolve([options.font], context), chart.options.font);
+    const opts = this.options.ticks.setContext(this.getContext(index));
+    return toFont(opts.font);
   }
 }
 
index 45df85a1d1777f91dc829b8cf4f568a1fd3a94cc..76306cbe0e71c1756ae01fae8392ea858fdd0c93 100644 (file)
@@ -88,6 +88,10 @@ function registerDefaults(item, scope, parentScope) {
   if (item.defaultRoutes) {
     routeDefaults(scope, item.defaultRoutes);
   }
+
+  if (item.descriptors) {
+    defaults.describe(scope, item.descriptors);
+  }
 }
 
 function routeDefaults(scope, routes) {
index 286008c347668f9834207a7046fb3ce93a7a6afe..d2123c425a67c9541564e8ea0878cf7bf49ae058 100644 (file)
@@ -214,7 +214,8 @@ ArcElement.defaults = {
   borderAlign: 'center',
   borderColor: '#fff',
   borderWidth: 2,
-  offset: 0
+  offset: 0,
+  angle: undefined
 };
 
 /**
index c12b7a45324a05098310ca0c16994abf4cd7eac1..3e2860ffd4d02d3f5aafa4d51ce99c4b553f7aa0 100644 (file)
@@ -256,7 +256,8 @@ BarElement.id = 'bar';
 BarElement.defaults = {
   borderSkipped: 'start',
   borderWidth: 0,
-  borderRadius: 0
+  borderRadius: 0,
+  pointStyle: undefined
 };
 
 /**
index 9aec82a729a08f48300b018e7921f3c3ea2018cd..08aa993b578e5a2af5e0e9d4f4de387f2f2a7bea 100644 (file)
@@ -395,8 +395,11 @@ LineElement.defaults = {
   borderJoinStyle: 'miter',
   borderWidth: 3,
   capBezierPoints: true,
+  cubicInterpolationMode: 'default',
   fill: false,
-  tension: 0
+  spanGaps: false,
+  stepped: false,
+  tension: 0,
 };
 
 /**
@@ -406,3 +409,9 @@ LineElement.defaultRoutes = {
   backgroundColor: 'backgroundColor',
   borderColor: 'borderColor'
 };
+
+
+LineElement.descriptors = {
+  _scriptable: true,
+  _indexable: (name) => name !== 'borderDash' && name !== 'fill',
+};
index 8ee204c29f08b192ff14a0a547d95aa221eb4a97..0190626a37f71ff9c6c75233c387ec9048af83a1 100644 (file)
@@ -77,7 +77,8 @@ PointElement.defaults = {
   hoverBorderWidth: 1,
   hoverRadius: 4,
   pointStyle: 'circle',
-  radius: 3
+  radius: 3,
+  rotation: 0
 };
 
 /**
diff --git a/src/helpers/helpers.config.js b/src/helpers/helpers.config.js
new file mode 100644 (file)
index 0000000..2f61a79
--- /dev/null
@@ -0,0 +1,230 @@
+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.
+ * @returns Proxy
+ * @private
+ */
+export function _createResolver(scopes, prefixes = ['']) {
+  const cache = {
+    [Symbol.toStringTag]: 'Object',
+    _cacheable: true,
+    _scopes: scopes,
+    override: (scope) => _createResolver([scope].concat(scopes), prefixes),
+  };
+  return new Proxy(cache, {
+    get(target, prop) {
+      return _cached(target, prop,
+        () => _resolveWithPrefixes(prop, prefixes, scopes));
+    },
+
+    ownKeys(target) {
+      return getKeysFromAllScopes(target);
+    },
+
+    getOwnPropertyDescriptor(target, prop) {
+      return Reflect.getOwnPropertyDescriptor(target._scopes[0], prop);
+    },
+
+    set(target, prop, value) {
+      scopes[0][prop] = value;
+      return delete target[prop];
+    }
+  });
+}
+
+/**
+ * Returns an Proxy for resolving option values with context.
+ * @param {object} proxy - The Proxy returned by `_createResolver`
+ * @param {object} context - Context object for scriptable/indexable options
+ * @param {object} [subProxy] - The proxy provided for scriptable options
+ * @private
+ */
+export function _attachContext(proxy, context, subProxy) {
+  const cache = {
+    _cacheable: false,
+    _proxy: proxy,
+    _context: context,
+    _subProxy: subProxy,
+    _stack: new Set(),
+    _descriptors: _descriptors(proxy),
+    setContext: (ctx) => _attachContext(proxy, ctx, subProxy),
+    override: (scope) => _attachContext(proxy.override(scope), context, subProxy)
+  };
+  return new Proxy(cache, {
+    get(target, prop, receiver) {
+      return _cached(target, prop,
+        () => _resolveWithContext(target, prop, receiver));
+    },
+
+    ownKeys() {
+      return Reflect.ownKeys(proxy);
+    },
+
+    getOwnPropertyDescriptor(target, prop) {
+      return Reflect.getOwnPropertyDescriptor(proxy._scopes[0], prop);
+    },
+
+    set(target, prop, value) {
+      proxy[prop] = value;
+      return delete target[prop];
+    }
+  });
+}
+
+/**
+ * @private
+ */
+export function _descriptors(proxy) {
+  const {_scriptable = true, _indexable = true} = proxy;
+  return {
+    isScriptable: isFunction(_scriptable) ? _scriptable : () => _scriptable,
+    isIndexable: isFunction(_indexable) ? _indexable : () => _indexable
+  };
+}
+
+const readKey = (prefix, name) => prefix ? prefix + _capitalize(name) : name;
+const needsSubResolver = (prop, value) => isObject(value);
+
+function _cached(target, prop, resolve) {
+  let value = target[prop]; // cached value
+  if (defined(value)) {
+    return value;
+  }
+
+  value = resolve();
+
+  if (defined(value)) {
+    // cache the resolved value
+    target[prop] = value;
+  }
+  return value;
+}
+
+function _resolveWithContext(target, prop, receiver) {
+  const {_proxy, _context, _subProxy, _descriptors: descriptors} = target;
+  let value = _proxy[prop]; // resolve from proxy
+
+  // resolve with context
+  if (isFunction(value) && descriptors.isScriptable(prop)) {
+    value = _resolveScriptable(prop, value, target, receiver);
+  }
+  if (isArray(value) && value.length) {
+    value = _resolveArray(prop, value, target, descriptors.isIndexable);
+  }
+  if (needsSubResolver(prop, value)) {
+    // if the resolved value is an object, crate a sub resolver for it
+    value = _attachContext(value, _context, _subProxy && _subProxy[prop]);
+  }
+  return value;
+}
+
+function _resolveScriptable(prop, value, target, receiver) {
+  const {_proxy, _context, _subProxy, _stack} = target;
+  if (_stack.has(prop)) {
+    // @ts-ignore
+    throw new Error('Recursion detected: ' + [..._stack].join('->') + '->' + prop);
+  }
+  _stack.add(prop);
+  value = value(_context, _subProxy || receiver);
+  _stack.delete(prop);
+  if (isObject(value)) {
+    // When scriptable option returns an object, create a resolver on that.
+    value = createSubResolver([value].concat(_proxy._scopes), prop, value);
+  }
+  return value;
+}
+
+function _resolveArray(prop, value, target, isIndexable) {
+  const {_proxy, _context, _subProxy} = target;
+
+  if (defined(_context.index) && isIndexable(prop)) {
+    value = value[_context.index % value.length];
+  } else if (isObject(value[0])) {
+    // Array of objects, return array or resolvers
+    const arr = value;
+    const scopes = _proxy._scopes.filter(s => s !== arr);
+    value = [];
+    for (const item of arr) {
+      const resolver = createSubResolver([item].concat(scopes), prop, item);
+      value.push(_attachContext(resolver, _context, _subProxy && _subProxy[prop]));
+    }
+  }
+  return value;
+}
+
+function createSubResolver(parentScopes, prop, value) {
+  const set = new Set([value]);
+  const {keys, includeParents} = _resolveSubKeys(parentScopes, prop, value);
+  for (const key of keys) {
+    for (const item of parentScopes) {
+      const scope = resolveObjectKey(item, key);
+      if (scope) {
+        set.add(scope);
+      } 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;
+      }
+    }
+  }
+  if (includeParents) {
+    parentScopes.forEach(set.add, set);
+  }
+  return _createResolver([...set]);
+}
+
+function _resolveSubKeys(parentScopes, prop, value) {
+  const fallback = _resolve('_fallback', parentScopes.map(scope => scope[prop] || scope));
+  const keys = [prop];
+  if (defined(fallback)) {
+    const resolved = isFunction(fallback) ? fallback(prop, value) : fallback;
+    keys.push(...(isArray(resolved) ? resolved : [resolved]));
+  }
+  return {keys: keys.filter(v => v), includeParents: fallback !== prop};
+}
+
+function _resolveWithPrefixes(prop, prefixes, scopes) {
+  let value;
+  for (const prefix of prefixes) {
+    value = _resolve(readKey(prefix, prop), scopes);
+    if (defined(value)) {
+      return (needsSubResolver(prop, value))
+        ? createSubResolver(scopes, prop, value)
+        : value;
+    }
+  }
+}
+
+function _resolve(key, scopes) {
+  for (const scope of scopes) {
+    if (!scope) {
+      continue;
+    }
+    const value = scope[key];
+    if (defined(value)) {
+      return value;
+    }
+  }
+}
+
+function getKeysFromAllScopes(target) {
+  let keys = target._keys;
+  if (!keys) {
+    keys = target._keys = resolveKeysFromAllScopes(target._scopes);
+  }
+  return keys;
+}
+
+function resolveKeysFromAllScopes(scopes) {
+  const set = new Set();
+  for (const scope of scopes) {
+    for (const key of Object.keys(scope).filter(k => !k.startsWith('_'))) {
+      set.add(key);
+    }
+  }
+  return [...set];
+}
index a0c63fde08ba67e0a49a2f4fb43f3cc000ccfad1..20c0f6e3bdf13d2047623ed31b197a70c6befe73 100644 (file)
@@ -309,3 +309,8 @@ export function resolveObjectKey(obj, key) {
 export function _capitalize(str) {
   return str.charAt(0).toUpperCase() + str.slice(1);
 }
+
+
+export const defined = (value) => typeof value !== 'undefined';
+
+export const isFunction = (value) => typeof value === 'function';
index b45d88433ceb107aba06fe32372ccdcd0ddd506a..9861601c4da7ef3c55e07ffac41e640b8fe5022c 100644 (file)
@@ -1,6 +1,7 @@
 export * from './helpers.core';
 export * from './helpers.canvas';
 export * from './helpers.collection';
+export * from './helpers.config';
 export * from './helpers.curve';
 export * from './helpers.dom';
 export {default as easingEffects} from './helpers.easing';
index 5b5851ebab70a1ff97991dfde8c685f9ff71b4c4..44803cdbd23a598b4c0a88c7af07dea28fedf561 100644 (file)
@@ -11,9 +11,9 @@ export default class BasePlatform {
         * Called at chart construction time, returns a context2d instance implementing
         * the [W3C Canvas 2D Context API standard]{@link https://www.w3.org/TR/2dcontext/}.
         * @param {HTMLCanvasElement} canvas - The canvas from which to acquire context (platform specific)
-        * @param {object} options - The chart options
+        * @param {number} [aspectRatio] - The chart options
         */
-  acquireContext(canvas, options) {} // eslint-disable-line no-unused-vars
+  acquireContext(canvas, aspectRatio) {} // eslint-disable-line no-unused-vars
 
   /**
         * Called at chart destruction time, releases any resources associated to the context
index 1b6fb13f7e35fba9016f41b78d6913a984358e01..7cbe719f27d01c56e4b890e8fc0c80175438c12b 100644 (file)
@@ -36,9 +36,9 @@ const isNullOrEmpty = value => value === null || value === '';
  * since responsiveness is handled by the controller.resize() method. The config is used
  * to determine the aspect ratio to apply in case no explicit height has been specified.
  * @param {HTMLCanvasElement} canvas
- * @param {{ options: any; }} config
+ * @param {number} [aspectRatio]
  */
-function initCanvas(canvas, config) {
+function initCanvas(canvas, aspectRatio) {
   const style = canvas.style;
 
   // NOTE(SB) canvas.getAttribute('width') !== canvas.width: in the first case it
@@ -78,7 +78,7 @@ function initCanvas(canvas, config) {
       // If no explicit render height and style height, let's apply the aspect ratio,
       // which one can be specified by the user but also by charts as default option
       // (i.e. options.aspectRatio). If not specified, use canvas aspect ratio of 2.
-      canvas.height = canvas.width / (config.options.aspectRatio || 2);
+      canvas.height = canvas.width / (aspectRatio || 2);
     } else {
       const displayHeight = readUsedSize(canvas, 'height');
       if (displayHeight !== undefined) {
@@ -259,10 +259,10 @@ export default class DomPlatform extends BasePlatform {
 
   /**
         * @param {HTMLCanvasElement} canvas
-        * @param {{ options: { aspectRatio?: number; }; }} config
+        * @param {number} [aspectRatio]
         * @return {CanvasRenderingContext2D|null}
         */
-  acquireContext(canvas, config) {
+  acquireContext(canvas, aspectRatio) {
     // To prevent canvas fingerprinting, some add-ons undefine the getContext
     // method, for example: https://github.com/kkapsner/CanvasBlocker
     // https://github.com/chartjs/Chart.js/issues/2807
@@ -278,7 +278,7 @@ export default class DomPlatform extends BasePlatform {
     if (context && context.canvas === canvas) {
       // Load platform resources on first chart creation, to make it possible to
       // import the library before setting platform options.
-      initCanvas(canvas, config);
+      initCanvas(canvas, aspectRatio);
       return context;
     }
 
index af3eaef5b32ebb4ac69c1e0350bc2d93f6c24ac8..ffa8dfc3d4539b3d14f51ee33d473d1285aeb6c6 100644 (file)
@@ -128,7 +128,7 @@ export class Legend extends Element {
     }
 
     const labelOpts = options.labels;
-    const labelFont = toFont(labelOpts.font, me.chart.options.font);
+    const labelFont = toFont(labelOpts.font);
     const fontSize = labelFont.size;
     const titleHeight = me._computeTitleHeight();
     const {boxWidth, itemHeight} = getBoxSize(labelOpts, fontSize);
@@ -241,7 +241,7 @@ export class Legend extends Element {
     const {align, labels: labelOpts} = opts;
     const defaultColor = defaults.color;
     const rtlHelper = getRtlAdapter(opts.rtl, me.left, me.width);
-    const labelFont = toFont(labelOpts.font, me.chart.options.font);
+    const labelFont = toFont(labelOpts.font);
     const {color: fontColor, padding} = labelOpts;
     const fontSize = labelFont.size;
     let cursor;
@@ -378,7 +378,7 @@ export class Legend extends Element {
     const me = this;
     const opts = me.options;
     const titleOpts = opts.title;
-    const titleFont = toFont(titleOpts.font, me.chart.options.font);
+    const titleFont = toFont(titleOpts.font);
     const titlePadding = toPadding(titleOpts.padding);
 
     if (!titleOpts.display) {
@@ -427,7 +427,7 @@ export class Legend extends Element {
         */
   _computeTitleHeight() {
     const titleOpts = this.options.title;
-    const titleFont = toFont(titleOpts.font, this.chart.options.font);
+    const titleFont = toFont(titleOpts.font);
     const titlePadding = toPadding(titleOpts.padding);
     return titleOpts.display ? titleFont.lineHeight + titlePadding.height : 0;
   }
@@ -614,5 +614,15 @@ export default {
   defaultRoutes: {
     'labels.color': 'color',
     'title.color': 'color'
-  }
+  },
+
+  descriptors: {
+    _scriptable: (name) => !name.startsWith('on'),
+    labels: {
+      _scriptable: false,
+    }
+  },
+
+  // For easier configuration, resolve additionally from root of options and defaults.
+  additionalOptionScopes: ['']
 };
index 6543f5b29cc20dcc2b7be33607e7969bb2cc5fb5..04e6917969d0eb46ba82f7afabfd70610aa3516b 100644 (file)
@@ -43,7 +43,7 @@ export class Title extends Element {
 
     const lineCount = isArray(opts.text) ? opts.text.length : 1;
     me._padding = toPadding(opts.padding);
-    const textSize = lineCount * toFont(opts.font, me.chart.options.font).lineHeight + me._padding.height;
+    const textSize = lineCount * toFont(opts.font).lineHeight + me._padding.height;
 
     if (me.isHorizontal()) {
       me.height = textSize;
@@ -91,7 +91,7 @@ export class Title extends Element {
       return;
     }
 
-    const fontOpts = toFont(opts.font, me.chart.options.font);
+    const fontOpts = toFont(opts.font);
     const lineHeight = fontOpts.lineHeight;
     const offset = lineHeight / 2 + me._padding.top;
     const {titleX, titleY, maxWidth, rotation} = me._drawArgs(offset);
@@ -179,5 +179,8 @@ export default {
 
   defaultRoutes: {
     color: 'color'
-  }
+  },
+
+  // For easier configuration, resolve additionally from root of options and defaults.
+  additionalOptionScopes: ['']
 };
index 566481554f404c617a3bd1c13ee3f7e0ff7f2bcf..7edffa7f27ef64d42bd597ea4cab400738fa8cf7 100644 (file)
@@ -1,6 +1,6 @@
 import Animations from '../core/core.animations';
 import Element from '../core/core.element';
-import {each, noop, isNullOrUndef, isArray, _elementsEqual, valueOrDefault} from '../helpers/helpers.core';
+import {each, noop, isNullOrUndef, isArray, _elementsEqual} from '../helpers/helpers.core';
 import {getRtlAdapter, overrideTextDirection, restoreTextDirection} from '../helpers/helpers.rtl';
 import {distanceBetweenPoints} from '../helpers/helpers.math';
 import {drawPoint, toFontString} from '../helpers';
@@ -368,9 +368,6 @@ export class Tooltip extends Element {
   }
 
   initialize(options) {
-    const defaultSize = options.bodyFont.size;
-    options.boxHeight = valueOrDefault(options.boxHeight, defaultSize);
-    options.boxWidth = valueOrDefault(options.boxWidth, defaultSize);
     this.options = options;
     this._cachedAnimations = undefined;
   }
@@ -1102,6 +1099,8 @@ export default {
     caretPadding: 2,
     caretSize: 5,
     cornerRadius: 6,
+    boxHeight: (ctx, opts) => opts.bodyFont.size,
+    boxWidth: (ctx, opts) => opts.bodyFont.size,
     multiKeyBackground: '#fff',
     displayColors: true,
     borderColor: 'rgba(0,0,0,0)',
@@ -1196,5 +1195,17 @@ export default {
     bodyFont: 'font',
     footerFont: 'font',
     titleFont: 'font'
-  }
+  },
+
+  descriptors: {
+    _scriptable: (name) => name !== 'filter' && name !== 'itemSort' && name !== 'custom',
+    _indexable: false,
+    callbacks: {
+      _scriptable: false,
+      _indexable: false,
+    }
+  },
+
+  // For easier configuration, resolve additionally from `interaction` and root of options and defaults.
+  additionalOptionScopes: ['interaction', '']
 };
index 45e938e29385769778dbfb9bba71885844f192de..61415d7edf6591dd662ce859a71301d8c816dab0 100644 (file)
@@ -4,7 +4,7 @@ import {HALF_PI, isNumber, TAU, toDegrees, toRadians, _normalizeAngle} from '../
 import LinearScaleBase from './scale.linearbase';
 import Ticks from '../core/core.ticks';
 import {valueOrDefault, isArray, isFinite, callback as callCallback, isNullOrUndef} from '../helpers/helpers.core';
-import {toFont, resolve} from '../helpers/helpers.options';
+import {toFont} from '../helpers/helpers.options';
 
 function getTickBackdropHeight(opts) {
   const tickOpts = opts.ticks;
@@ -95,9 +95,8 @@ function fitWithPointLabels(scale) {
   const valueCount = scale.chart.data.labels.length;
   for (i = 0; i < valueCount; i++) {
     pointPosition = scale.getPointPosition(i, scale.drawingArea + 5);
-
-    const context = scale.getContext(i);
-    const plFont = toFont(resolve([scale.options.pointLabels.font], context, i), scale.chart.options.font);
+    const opts = scale.options.pointLabels.setContext(scale.getContext(i));
+    const plFont = toFont(opts.font);
     scale.ctx.font = plFont.string;
     textSize = measureLabelSize(scale.ctx, plFont.lineHeight, scale.pointLabels[i]);
     scale._pointLabelSizes[i] = textSize;
@@ -166,8 +165,8 @@ function drawPointLabels(scale) {
     const extra = (i === 0 ? tickBackdropHeight / 2 : 0);
     const pointLabelPosition = scale.getPointPosition(i, outerDistance + extra + 5);
 
-    const context = scale.getContext(i);
-    const plFont = toFont(resolve([pointLabelOpts.font], context, i), scale.chart.options.font);
+    const optsAtIndex = pointLabelOpts.setContext(scale.getContext(i));
+    const plFont = toFont(optsAtIndex.font);
     const angle = toDegrees(scale.getIndexAngle(i));
     adjustPointPositionForLabelHeight(angle, scale._pointLabelSizes[i], pointLabelPosition);
     renderText(
@@ -177,7 +176,7 @@ function drawPointLabels(scale) {
       pointLabelPosition.y + (plFont.lineHeight / 2),
       plFont,
       {
-        color: resolve([pointLabelOpts.color], context, i),
+        color: optsAtIndex.color,
         textAlign: getTextAlignForAngle(angle),
       }
     );
@@ -185,14 +184,13 @@ function drawPointLabels(scale) {
   ctx.restore();
 }
 
-function drawRadiusLine(scale, gridLineOpts, radius, index) {
+function drawRadiusLine(scale, gridLineOpts, radius) {
   const ctx = scale.ctx;
   const circular = gridLineOpts.circular;
   const valueCount = scale.chart.data.labels.length;
 
-  const context = scale.getContext(index);
-  const lineColor = resolve([gridLineOpts.color], context, index - 1);
-  const lineWidth = resolve([gridLineOpts.lineWidth], context, index - 1);
+  const lineColor = gridLineOpts.color;
+  const lineWidth = gridLineOpts.lineWidth;
   let pointPosition;
 
   if ((!circular && !valueCount) || !lineColor || !lineWidth || radius < 0) {
@@ -202,10 +200,8 @@ function drawRadiusLine(scale, gridLineOpts, radius, index) {
   ctx.save();
   ctx.strokeStyle = lineColor;
   ctx.lineWidth = lineWidth;
-  if (ctx.setLineDash) {
-    ctx.setLineDash(resolve([gridLineOpts.borderDash, []], context));
-    ctx.lineDashOffset = resolve([gridLineOpts.borderDashOffset], context, index - 1);
-  }
+  ctx.setLineDash(gridLineOpts.borderDash);
+  ctx.lineDashOffset = gridLineOpts.borderDashOffset;
 
   ctx.beginPath();
   if (circular) {
@@ -245,11 +241,6 @@ export default class RadialLinearScale extends LinearScaleBase {
     this.pointLabels = [];
   }
 
-  init(options) {
-    super.init(options);
-    this.axis = 'r';
-  }
-
   setDimensions() {
     const me = this;
 
@@ -408,7 +399,8 @@ export default class RadialLinearScale extends LinearScaleBase {
       me.ticks.forEach((tick, index) => {
         if (index !== 0) {
           offset = me.getDistanceFromCenterForValue(me.ticks[index].value);
-          drawRadiusLine(me, gridLineOpts, offset, index);
+          const optsAtIndex = gridLineOpts.setContext(me.getContext(index - 1));
+          drawRadiusLine(me, optsAtIndex, offset);
         }
       });
     }
@@ -417,9 +409,9 @@ export default class RadialLinearScale extends LinearScaleBase {
       ctx.save();
 
       for (i = me.chart.data.labels.length - 1; i >= 0; i--) {
-        const context = me.getContext(i);
-        const lineWidth = resolve([angleLineOpts.lineWidth, gridLineOpts.lineWidth], context, i);
-        const color = resolve([angleLineOpts.color, gridLineOpts.color], context, i);
+        const optsAtIndex = angleLineOpts.setContext(me.getContext(i));
+        const lineWidth = optsAtIndex.lineWidth;
+        const color = optsAtIndex.color;
 
         if (!lineWidth || !color) {
           continue;
@@ -428,10 +420,8 @@ export default class RadialLinearScale extends LinearScaleBase {
         ctx.lineWidth = lineWidth;
         ctx.strokeStyle = color;
 
-        if (ctx.setLineDash) {
-          ctx.setLineDash(resolve([angleLineOpts.borderDash, gridLineOpts.borderDash, []], context));
-          ctx.lineDashOffset = resolve([angleLineOpts.borderDashOffset, gridLineOpts.borderDashOffset, 0.0], context, i);
-        }
+        ctx.setLineDash(optsAtIndex.borderDash);
+        ctx.lineDashOffset = optsAtIndex.borderDashOffset;
 
         offset = me.getDistanceFromCenterForValue(opts.ticks.reverse ? me.min : me.max);
         position = me.getPointPosition(i, offset);
@@ -472,26 +462,24 @@ export default class RadialLinearScale extends LinearScaleBase {
         return;
       }
 
-      const context = me.getContext(index);
-      const tickFont = me._resolveTickFontOptions(index);
+      const optsAtIndex = tickOpts.setContext(me.getContext(index));
+      const tickFont = toFont(optsAtIndex.font);
       offset = me.getDistanceFromCenterForValue(me.ticks[index].value);
 
-      const showLabelBackdrop = resolve([tickOpts.showLabelBackdrop], context, index);
-
-      if (showLabelBackdrop) {
+      if (optsAtIndex.showLabelBackdrop) {
         width = ctx.measureText(tick.label).width;
-        ctx.fillStyle = resolve([tickOpts.backdropColor], context, index);
+        ctx.fillStyle = optsAtIndex.backdropColor;
 
         ctx.fillRect(
-          -width / 2 - tickOpts.backdropPaddingX,
-          -offset - tickFont.size / 2 - tickOpts.backdropPaddingY,
-          width + tickOpts.backdropPaddingX * 2,
-          tickFont.size + tickOpts.backdropPaddingY * 2
+          -width / 2 - optsAtIndex.backdropPaddingX,
+          -offset - tickFont.size / 2 - optsAtIndex.backdropPaddingY,
+          width + optsAtIndex.backdropPaddingX * 2,
+          tickFont.size + optsAtIndex.backdropPaddingY * 2
         );
       }
 
       renderText(ctx, tick.label, 0, -offset, tickFont, {
-        color: tickOpts.color,
+        color: optsAtIndex.color,
       });
     });
 
@@ -565,3 +553,9 @@ RadialLinearScale.defaultRoutes = {
   'pointLabels.color': 'color',
   'ticks.color': 'color'
 };
+
+RadialLinearScale.descriptors = {
+  angleLines: {
+    _fallback: 'gridLines'
+  }
+};
index fb1dcd527e4bb8a36ab23fb6c5625aa861764d04..535433fedaa73c484e43866a9f3a5624b222880f 100644 (file)
@@ -11,10 +11,10 @@ module.exports = {
           gridLines: {
             display: true,
             color: function(context) {
-              return context.index % 2 === 0 ? 'red' : 'green';
+              return context.index % 2 === 0 ? 'green' : 'red';
             },
             lineWidth: function(context) {
-              return context.index % 2 === 0 ? 1 : 5;
+              return context.index % 2 === 0 ? 5 : 1;
             },
           },
           angleLines: {
index b991c6cc445c8eba85e3a17226b7ef4b58b95cb0..b69a5819b940eef991a9bcc26f92c1ec0c81c360 100644 (file)
@@ -1381,7 +1381,7 @@ describe('Chart.controllers.bar', function() {
       var meta = chart.getDatasetMeta(0);
       var yScale = chart.scales[meta.yAxisID];
 
-      var config = meta.controller._config;
+      var config = meta.controller.options;
       var categoryPercentage = config.categoryPercentage;
       var barPercentage = config.barPercentage;
       var stacked = yScale.options.stacked;
index d7b2c19390219b3fd978872a939d9858674ed954..6cb2d3aac4d8385da0b00e4c23e98f5d4b573a4b 100644 (file)
@@ -11,7 +11,7 @@ describe('Chart.animations', function() {
       }
     });
     expect(anims._properties.get('property1')).toEqual(jasmine.objectContaining({duration: 1000}));
-    expect(anims._properties.get('property2')).toEqual({duration: 2000});
+    expect(anims._properties.get('property2')).toEqual(jasmine.objectContaining({duration: 2000}));
   });
 
   it('should ignore duplicate definitions from collections', function() {
@@ -52,7 +52,7 @@ describe('Chart.animations', function() {
   });
 
   it('should clone the target options, if those are shared and new options are not', function() {
-    const chart = {};
+    const chart = {options: {}};
     const anims = new Chart.Animations(chart, {option: {duration: 200}});
     const options = {option: 0, $shared: true};
     const target = {options};
@@ -138,7 +138,6 @@ describe('Chart.animations', function() {
     }, 50);
   });
 
-
   it('should assign final shared options to target after animations complete', function(done) {
     const chart = {
       draw: function() {},
index 6d67b9c2cf0853f737df058a62cc749d24da0d76..27736973b1f00c93e8d58dfc180d33f12aa703d0 100644 (file)
@@ -104,7 +104,7 @@ describe('Chart', function() {
 
       var options = chart.options;
       expect(options.font.size).toBe(defaults.font.size);
-      expect(options.showLine).toBe(defaults.controllers.line.showLine);
+      expect(options.showLine).toBe(defaults.controllers.line.datasets.showLine);
       expect(options.spanGaps).toBe(true);
       expect(options.hover.onHover).toBe(callback);
       expect(options.hover.mode).toBe('test');
@@ -128,7 +128,7 @@ describe('Chart', function() {
 
       var options = chart.options;
       expect(options.font.size).toBe(defaults.font.size);
-      expect(options.showLine).toBe(defaults.controllers.line.showLine);
+      expect(options.showLine).toBe(defaults.controllers.line.datasets.showLine);
       expect(options.spanGaps).toBe(true);
       expect(options.hover.onHover).toBe(callback);
       expect(options.hover.mode).toBe('test');
@@ -162,7 +162,6 @@ describe('Chart', function() {
       });
 
       var options = chart.options;
-      expect(options.showLine).toBe(defaults.showLine);
       expect(options.spanGaps).toBe(false);
       expect(options.hover.mode).toBe('dataset');
       expect(options.plugins.title.position).toBe('bottom');
@@ -1252,7 +1251,7 @@ describe('Chart', function() {
         options: {
           responsive: true,
           scales: {
-            y: {
+            yAxis0: {
               min: 0,
               max: 10
             }
@@ -1298,7 +1297,7 @@ describe('Chart', function() {
       chart.options.plugins.tooltip = newTooltipConfig;
 
       chart.update();
-      expect(chart.tooltip.options).toEqual(jasmine.objectContaining(newTooltipConfig));
+      expect(chart.tooltip.options).toEqualOptions(newTooltipConfig);
     });
 
     it ('should update the tooltip on update', async function() {
index 2b34a8fa36cb18d1ebfeb426743a8b7430f39b92..028b3077ffe2ee35ba7896cec5b7c2a4c027c7d9 100644 (file)
@@ -674,85 +674,6 @@ describe('Chart.DatasetController', function() {
     Chart.defaults.borderColor = oldColor;
   });
 
-  describe('_resolveOptions', function() {
-    it('should resove names in array notation', function() {
-      Chart.defaults.elements.line.globalTest = 'global';
-
-      const chart = acquireChart({
-        type: 'line',
-        data: {
-          datasets: [{
-            data: [1],
-            datasetTest: 'dataset'
-          }]
-        },
-        options: {
-          elements: {
-            line: {
-              elementTest: 'element'
-            }
-          }
-        }
-      });
-
-      const controller = chart.getDatasetMeta(0).controller;
-
-      expect(controller._resolveOptions(
-        [
-          'datasetTest',
-          'elementTest',
-          'globalTest'
-        ],
-        {type: 'line'})
-      ).toEqual({
-        datasetTest: 'dataset',
-        elementTest: 'element',
-        globalTest: 'global'
-      });
-
-      // Remove test from global defaults
-      delete Chart.defaults.elements.line.globalTest;
-    });
-
-    it('should resove names in object notation', function() {
-      Chart.defaults.elements.line.global = 'global';
-
-      const chart = acquireChart({
-        type: 'line',
-        data: {
-          datasets: [{
-            data: [1],
-            datasetTest: 'dataset'
-          }]
-        },
-        options: {
-          elements: {
-            line: {
-              element: 'element'
-            }
-          }
-        }
-      });
-
-      const controller = chart.getDatasetMeta(0).controller;
-
-      expect(controller._resolveOptions(
-        {
-          dataset: 'datasetTest',
-          element: 'elementTest',
-          global: 'globalTest'},
-        {type: 'line'})
-      ).toEqual({
-        dataset: 'dataset',
-        element: 'element',
-        global: 'global'
-      });
-
-      // Remove test from global defaults
-      delete Chart.defaults.elements.line.global;
-    });
-  });
-
   describe('resolveDataElementOptions', function() {
     it('should cache options when possible', function() {
       const chart = acquireChart({
index d38e55619d8b15eb7088da649db0002a6dfd8d2a..9a70f10a7e883281b65d78b087952cbf65bdb62c 100644 (file)
@@ -13,7 +13,7 @@ describe('Chart.plugins', function() {
       expect(plugin.hook.calls.count()).toBe(1);
       expect(plugin.hook.calls.first().args[0]).toBe(chart);
       expect(plugin.hook.calls.first().args[1]).toBe(args);
-      expect(plugin.hook.calls.first().args[2]).toEqual({});
+      expect(plugin.hook.calls.first().args[2]).toEqualOptions({});
     });
 
     it('should call global plugins with arguments', function() {
@@ -28,7 +28,7 @@ describe('Chart.plugins', function() {
       expect(plugin.hook.calls.count()).toBe(1);
       expect(plugin.hook.calls.first().args[0]).toBe(chart);
       expect(plugin.hook.calls.first().args[1]).toBe(args);
-      expect(plugin.hook.calls.first().args[2]).toEqual({});
+      expect(plugin.hook.calls.first().args[2]).toEqualOptions({});
       Chart.unregister(plugin);
     });
 
@@ -181,9 +181,9 @@ describe('Chart.plugins', function() {
       chart.notifyPlugins('hook', {arg1: 'bla', arg2: 42});
 
       expect(plugin.hook.calls.count()).toBe(3);
-      expect(plugin.hook.calls.argsFor(0)[2]).toEqual({a: '123'});
-      expect(plugin.hook.calls.argsFor(1)[2]).toEqual({a: '123'});
-      expect(plugin.hook.calls.argsFor(2)[2]).toEqual({a: '123'});
+      expect(plugin.hook.calls.argsFor(0)[2]).toEqualOptions({a: '123'});
+      expect(plugin.hook.calls.argsFor(1)[2]).toEqualOptions({a: '123'});
+      expect(plugin.hook.calls.argsFor(2)[2]).toEqualOptions({a: '123'});
 
       Chart.unregister(plugin);
     });
@@ -217,9 +217,9 @@ describe('Chart.plugins', function() {
       expect(plugins.a.hook).toHaveBeenCalled();
       expect(plugins.b.hook).toHaveBeenCalled();
       expect(plugins.c.hook).toHaveBeenCalled();
-      expect(plugins.a.hook.calls.first().args[2]).toEqual({a: '123'});
-      expect(plugins.b.hook.calls.first().args[2]).toEqual({b: '456'});
-      expect(plugins.c.hook.calls.first().args[2]).toEqual({c: '789'});
+      expect(plugins.a.hook.calls.first().args[2]).toEqualOptions({a: '123'});
+      expect(plugins.b.hook.calls.first().args[2]).toEqualOptions({b: '456'});
+      expect(plugins.c.hook.calls.first().args[2]).toEqualOptions({c: '789'});
 
       Chart.unregister(plugins.a);
     });
@@ -274,7 +274,7 @@ describe('Chart.plugins', function() {
       chart.notifyPlugins('hook');
 
       expect(plugin.hook).toHaveBeenCalled();
-      expect(plugin.hook.calls.first().args[2]).toEqual({a: 42});
+      expect(plugin.hook.calls.first().args[2]).toEqualOptions({a: 42});
 
       Chart.unregister(plugin);
     });
@@ -291,7 +291,7 @@ describe('Chart.plugins', function() {
       chart.notifyPlugins('hook');
 
       expect(plugin.hook).toHaveBeenCalled();
-      expect(plugin.hook.calls.first().args[2]).toEqual({a: 'foobar'});
+      expect(plugin.hook.calls.first().args[2]).toEqualOptions({a: 'foobar'});
 
       Chart.unregister(plugin);
     });
@@ -315,7 +315,7 @@ describe('Chart.plugins', function() {
       chart.notifyPlugins('hook');
 
       expect(plugin.hook).toHaveBeenCalled();
-      expect(plugin.hook.calls.first().args[2]).toEqual({foo: 'foo'});
+      expect(plugin.hook.calls.first().args[2]).toEqualOptions({foo: 'foo'});
 
       chart.options.plugins.a = {bar: 'bar'};
       chart.update();
@@ -324,7 +324,7 @@ describe('Chart.plugins', function() {
       chart.notifyPlugins('hook');
 
       expect(plugin.hook).toHaveBeenCalled();
-      expect(plugin.hook.calls.first().args[2]).toEqual({bar: 'bar'});
+      expect(plugin.hook.calls.first().args[2]).toEqualOptions({bar: 'bar'});
     });
 
     it('should disable all plugins', function() {
diff --git a/test/specs/helpers.config.tests.js b/test/specs/helpers.config.tests.js
new file mode 100644 (file)
index 0000000..37213dc
--- /dev/null
@@ -0,0 +1,293 @@
+describe('Chart.helpers.config', function() {
+  const {getHoverColor, _createResolver, _attachContext} = Chart.helpers;
+
+  describe('_createResolver', function() {
+    it('should resolve to raw values', function() {
+      const defaults = {
+        color: 'red',
+        backgroundColor: 'green',
+        hoverColor: (ctx, options) => getHoverColor(options.color)
+      };
+      const options = {
+        color: 'blue'
+      };
+      const resolver = _createResolver([options, defaults]);
+      expect(resolver.color).toEqual('blue');
+      expect(resolver.backgroundColor).toEqual('green');
+      expect(resolver.hoverColor).toEqual(defaults.hoverColor);
+    });
+
+    it('should resolve to parent scopes', function() {
+      const defaults = {
+        root: true,
+        sub: {
+          child: true
+        }
+      };
+      const options = {
+        child: 'sub default comes before this',
+        opt: 'opt'
+      };
+      const resolver = _createResolver([options, defaults]);
+      const sub = resolver.sub;
+      expect(sub.root).toEqual(true);
+      expect(sub.child).toEqual(true);
+      expect(sub.opt).toEqual('opt');
+    });
+
+    it('should follow _fallback', function() {
+      const defaults = {
+        interaction: {
+          mode: 'test',
+          priority: 'fall'
+        },
+        hover: {
+          _fallback: 'interaction',
+          priority: 'main'
+        }
+      };
+      const options = {
+        interaction: {
+          a: 1
+        },
+        hover: {
+          b: 2
+        }
+      };
+      const resolver = _createResolver([options, defaults]);
+      expect(resolver.hover).toEqualOptions({
+        mode: 'test',
+        priority: 'main',
+        a: 1,
+        b: 2
+      });
+    });
+
+    it('should support overriding options', function() {
+      const defaults = {
+        option1: 'defaults1',
+        option2: 'defaults2',
+        option3: 'defaults3',
+      };
+      const options = {
+        option1: 'options1',
+        option2: 'options2'
+      };
+      const overrides = {
+        option1: 'override1'
+      };
+      const resolver = _createResolver([options, defaults]);
+      expect(resolver).toEqualOptions({
+        option1: 'options1',
+        option2: 'options2',
+        option3: 'defaults3'
+      });
+      expect(resolver.override(overrides)).toEqualOptions({
+        option1: 'override1',
+        option2: 'options2',
+        option3: 'defaults3'
+      });
+    });
+  });
+
+  describe('_attachContext', function() {
+    it('should resolve to final values', function() {
+      const defaults = {
+        color: 'red',
+        backgroundColor: 'green',
+        hoverColor: (ctx, options) => getHoverColor(options.color)
+      };
+      const options = {
+        color: ['white', 'blue']
+      };
+      const resolver = _createResolver([options, defaults]);
+      const opts = _attachContext(resolver, {index: 1});
+      expect(opts.color).toEqual('blue');
+      expect(opts.backgroundColor).toEqual('green');
+      expect(opts.hoverColor).toEqual(getHoverColor('blue'));
+    });
+
+    it('should thrown on recursion', function() {
+      const options = {
+        foo: (ctx, opts) => opts.bar,
+        bar: (ctx, opts) => opts.xyz,
+        xyz: (ctx, opts) => opts.foo
+      };
+      const resolver = _createResolver([options]);
+      const opts = _attachContext(resolver, {test: true});
+      expect(function() {
+        return opts.foo;
+      }).toThrowError('Recursion detected: foo->bar->xyz->foo');
+    });
+
+    it('should support scriptable options in subscopes', function() {
+      const defaults = {
+        elements: {
+          point: {
+            backgroundColor: 'red'
+          }
+        }
+      };
+      const options = {
+        elements: {
+          point: {
+            borderColor: (ctx, opts) => getHoverColor(opts.backgroundColor)
+          }
+        }
+      };
+      const resolver = _createResolver([options, defaults]);
+      const opts = _attachContext(resolver, {});
+      expect(opts.elements.point.borderColor).toEqual(getHoverColor('red'));
+      expect(opts.elements.point.backgroundColor).toEqual('red');
+    });
+
+    it('same resolver should be usable with multiple contexts', function() {
+      const defaults = {
+        animation: {
+          delay: 10
+        }
+      };
+      const options = {
+        animation: (ctx) => ctx.index === 0 ? {duration: 1000} : {duration: 500}
+      };
+      const resolver = _createResolver([options, defaults]);
+      const opts1 = _attachContext(resolver, {index: 0});
+      const opts2 = _attachContext(resolver, {index: 1});
+
+      expect(opts1.animation.duration).toEqual(1000);
+      expect(opts1.animation.delay).toEqual(10);
+
+      expect(opts2.animation.duration).toEqual(500);
+      expect(opts2.animation.delay).toEqual(10);
+    });
+
+    it('should fall back from object returned from scriptable option', function() {
+      const defaults = {
+        mainScope: {
+          main: true,
+          subScope: {
+            sub: true
+          }
+        }
+      };
+      const options = {
+        mainScope: (ctx) => ({
+          mainTest: ctx.contextValue,
+          subScope: {
+            subText: 'a'
+          }
+        })
+      };
+      const opts = _attachContext(_createResolver([options, defaults]), {contextValue: 'test'});
+      expect(opts.mainScope).toEqualOptions({
+        main: true,
+        mainTest: 'test',
+        subScope: {
+          sub: true,
+          subText: 'a'
+        }
+      });
+    });
+
+    it('should resolve array of non-indexable objects properly', function() {
+      const defaults = {
+        label: {
+          value: 42,
+          text: (ctx) => ctx.text
+        },
+        labels: {
+          _fallback: 'label',
+          _indexable: false
+        }
+      };
+
+      const options = {
+        labels: [{text: 'a'}, {text: 'b'}, {value: 1}]
+      };
+      const opts = _attachContext(_createResolver([options, defaults]), {text: 'context'});
+      expect(opts).toEqualOptions({
+        labels: [
+          {
+            text: 'a',
+            value: 42
+          },
+          {
+            text: 'b',
+            value: 42
+          },
+          {
+            text: 'context',
+            value: 1
+          }
+        ]
+      });
+    });
+
+    it('should support overriding options', function() {
+      const options = {
+        fn1: ctx => ctx.index,
+        fn2: ctx => ctx.type
+      };
+      const override = {
+        fn1: ctx => ctx.index * 2
+      };
+      const opts = _attachContext(_createResolver([options]), {index: 2, type: 'test'});
+      expect(opts).toEqualOptions({
+        fn1: 2,
+        fn2: 'test'
+      });
+      expect(opts.override(override)).toEqualOptions({
+        fn1: 4,
+        fn2: 'test'
+      });
+    });
+
+    it('should support changing context', function() {
+      const opts = _attachContext(_createResolver([{fn: ctx => ctx.test}]), {test: 1});
+      expect(opts.fn).toEqual(1);
+      expect(opts.setContext({test: 2}).fn).toEqual(2);
+      expect(opts.fn).toEqual(1);
+    });
+
+    describe('_indexable and _scriptable', function() {
+      it('should default to true', function() {
+        const options = {
+          array: [1, 2, 3],
+          func: (ctx) => ctx.index * 10
+        };
+        const opts = _attachContext(_createResolver([options]), {index: 1});
+        expect(opts.array).toEqual(2);
+        expect(opts.func).toEqual(10);
+      });
+
+      it('should allow false', function() {
+        const fn = () => 'test';
+        const options = {
+          _indexable: false,
+          _scriptable: false,
+          array: [1, 2, 3],
+          func: fn
+        };
+        const opts = _attachContext(_createResolver([options]), {index: 1});
+        expect(opts.array).toEqual([1, 2, 3]);
+        expect(opts.func).toEqual(fn);
+        expect(opts.func()).toEqual('test');
+      });
+
+      it('should allow function', function() {
+        const fn = () => 'test';
+        const options = {
+          _indexable: (prop) => prop !== 'array',
+          _scriptable: (prop) => prop === 'func',
+          array: [1, 2, 3],
+          array2: ['a', 'b', 'c'],
+          func: fn
+        };
+        const opts = _attachContext(_createResolver([options]), {index: 1});
+        expect(opts.array).toEqual([1, 2, 3]);
+        expect(opts.func).toEqual('test');
+        expect(opts.array2).toEqual('b');
+      });
+    });
+  });
+});
index 92bf482a07d13a40fa1d18d9015b654f94c685c7..a5d836949938519c7c4c5c7b910166a9724111a6 100644 (file)
@@ -620,7 +620,7 @@ describe('Legend block tests', function() {
       lineWidth: 5,
       strokeStyle: 'green',
       pointStyle: 'crossRot',
-      rotation: undefined,
+      rotation: 0,
       datasetIndex: 0
     }, {
       text: 'dataset2',
@@ -690,7 +690,7 @@ describe('Legend block tests', function() {
       lineWidth: 5,
       strokeStyle: 'green',
       pointStyle: 'star',
-      rotation: undefined,
+      rotation: 0,
       datasetIndex: 0
     }, {
       text: 'dataset2',
@@ -737,7 +737,7 @@ describe('Legend block tests', function() {
   });
 
   describe('config update', function() {
-    it ('should update the options', function() {
+    it('should update the options', function() {
       var chart = acquireChart({
         type: 'line',
         data: {
@@ -761,7 +761,7 @@ describe('Legend block tests', function() {
       expect(chart.legend.options.display).toBe(false);
     });
 
-    it ('should update the associated layout item', function() {
+    it('should update the associated layout item', function() {
       var chart = acquireChart({
         type: 'line',
         data: {},
@@ -790,7 +790,7 @@ describe('Legend block tests', function() {
       expect(chart.legend.weight).toBe(42);
     });
 
-    it ('should remove the legend if the new options are false', function() {
+    it('should remove the legend if the new options are false', function() {
       var chart = acquireChart({
         type: 'line',
         data: {
@@ -807,7 +807,7 @@ describe('Legend block tests', function() {
       expect(chart.legend).toBe(undefined);
     });
 
-    it ('should create the legend if the legend options are changed to exist', function() {
+    it('should create the legend if the legend options are changed to exist', function() {
       var chart = acquireChart({
         type: 'line',
         data: {
@@ -827,7 +827,7 @@ describe('Legend block tests', function() {
       chart.options.plugins.legend = {};
       chart.update();
       expect(chart.legend).not.toBe(undefined);
-      expect(chart.legend.options).toEqual(jasmine.objectContaining(Chart.defaults.plugins.legend));
+      expect(chart.legend.options).toEqualOptions(Chart.defaults.plugins.legend);
     });
   });
 
index 9bcd0e802c70fbbb701b24b6bd5607b6baf77927..fb9572380de47d87759d869afe8b9c71e8dc0f07 100644 (file)
@@ -350,7 +350,7 @@ describe('Title block tests', function() {
       chart.options.plugins.title = {};
       chart.update();
       expect(chart.titleBlock).not.toBe(undefined);
-      expect(chart.titleBlock.options).toEqual(jasmine.objectContaining(Chart.defaults.plugins.title));
+      expect(chart.titleBlock.options).toEqualOptions(Chart.defaults.plugins.title);
     });
   });
 });
index ba8f10fe068dcde636877e2bb7ea0b9242210921..c0fa90a1c365a97217c0913aa09d89e4d7695b46 100644 (file)
@@ -80,44 +80,44 @@ describe('Plugin.Tooltip', function() {
       expect(tooltip.yAlign).toEqual('center');
       expect(tooltip.options.bodyColor).toEqual('#fff');
 
-      expect(tooltip.options.bodyFont).toEqual(jasmine.objectContaining({
+      expect(tooltip.options.bodyFont).toEqualOptions({
         family: defaults.font.family,
         style: defaults.font.style,
         size: defaults.font.size,
-      }));
+      });
 
-      expect(tooltip.options).toEqual(jasmine.objectContaining({
+      expect(tooltip.options).toEqualOptions({
         bodyAlign: 'left',
         bodySpacing: 2,
-      }));
+      });
 
       expect(tooltip.options.titleColor).toEqual('#fff');
-      expect(tooltip.options.titleFont).toEqual(jasmine.objectContaining({
+      expect(tooltip.options.titleFont).toEqualOptions({
         family: defaults.font.family,
         style: 'bold',
         size: defaults.font.size,
-      }));
+      });
 
-      expect(tooltip.options).toEqual(jasmine.objectContaining({
+      expect(tooltip.options).toEqualOptions({
         titleAlign: 'left',
         titleSpacing: 2,
         titleMarginBottom: 6,
-      }));
+      });
 
       expect(tooltip.options.footerColor).toEqual('#fff');
-      expect(tooltip.options.footerFont).toEqual(jasmine.objectContaining({
+      expect(tooltip.options.footerFont).toEqualOptions({
         family: defaults.font.family,
         style: 'bold',
         size: defaults.font.size,
-      }));
+      });
 
-      expect(tooltip.options).toEqual(jasmine.objectContaining({
+      expect(tooltip.options).toEqualOptions({
         footerAlign: 'left',
         footerSpacing: 2,
         footerMarginTop: 6,
-      }));
+      });
 
-      expect(tooltip.options).toEqual(jasmine.objectContaining({
+      expect(tooltip.options).toEqualOptions({
         // Appearance
         caretSize: 5,
         caretPadding: 2,
@@ -125,7 +125,7 @@ describe('Plugin.Tooltip', function() {
         backgroundColor: 'rgba(0,0,0,0.8)',
         multiKeyBackground: '#fff',
         displayColors: true
-      }));
+      });
 
       expect(tooltip).toEqual(jasmine.objectContaining({
         opacity: 1,
@@ -245,10 +245,10 @@ describe('Plugin.Tooltip', function() {
       size: defaults.font.size,
     }));
 
-    expect(tooltip.options).toEqual(jasmine.objectContaining({
+    expect(tooltip.options).toEqualOptions({
       bodyAlign: 'left',
       bodySpacing: 2,
-    }));
+    });
 
     expect(tooltip.options.titleFont).toEqual(jasmine.objectContaining({
       family: defaults.font.family,
@@ -256,25 +256,25 @@ describe('Plugin.Tooltip', function() {
       size: defaults.font.size,
     }));
 
-    expect(tooltip.options).toEqual(jasmine.objectContaining({
+    expect(tooltip.options).toEqualOptions({
       titleAlign: 'left',
       titleSpacing: 2,
       titleMarginBottom: 6,
-    }));
+    });
 
-    expect(tooltip.options.footerFont).toEqual(jasmine.objectContaining({
+    expect(tooltip.options.footerFont).toEqualOptions({
       family: defaults.font.family,
       style: 'bold',
       size: defaults.font.size,
-    }));
+    });
 
-    expect(tooltip.options).toEqual(jasmine.objectContaining({
+    expect(tooltip.options).toEqualOptions({
       footerAlign: 'left',
       footerSpacing: 2,
       footerMarginTop: 6,
-    }));
+    });
 
-    expect(tooltip.options).toEqual(jasmine.objectContaining({
+    expect(tooltip.options).toEqualOptions({
       // Appearance
       caretSize: 5,
       caretPadding: 2,
@@ -282,7 +282,7 @@ describe('Plugin.Tooltip', function() {
       backgroundColor: 'rgba(0,0,0,0.8)',
       multiKeyBackground: '#fff',
       displayColors: true
-    }));
+    });
 
     expect(tooltip.opacity).toEqual(1);
     expect(tooltip.title).toEqual(['Point 2']);
@@ -395,10 +395,10 @@ describe('Plugin.Tooltip', function() {
       size: defaults.font.size,
     }));
 
-    expect(tooltip.options).toEqual(jasmine.objectContaining({
+    expect(tooltip.options).toEqualOptions({
       bodyAlign: 'left',
       bodySpacing: 2,
-    }));
+    });
 
     expect(tooltip.options.titleFont).toEqual(jasmine.objectContaining({
       family: defaults.font.family,
@@ -406,10 +406,10 @@ describe('Plugin.Tooltip', function() {
       size: defaults.font.size,
     }));
 
-    expect(tooltip.options).toEqual(jasmine.objectContaining({
+    expect(tooltip.options).toEqualOptions({
       titleSpacing: 2,
       titleMarginBottom: 6,
-    }));
+    });
 
     expect(tooltip.options.footerFont).toEqual(jasmine.objectContaining({
       family: defaults.font.family,
@@ -417,20 +417,20 @@ describe('Plugin.Tooltip', function() {
       size: defaults.font.size,
     }));
 
-    expect(tooltip.options).toEqual(jasmine.objectContaining({
+    expect(tooltip.options).toEqualOptions({
       footerAlign: 'left',
       footerSpacing: 2,
       footerMarginTop: 6,
-    }));
+    });
 
-    expect(tooltip.options).toEqual(jasmine.objectContaining({
+    expect(tooltip.options).toEqualOptions({
       // Appearance
       caretSize: 5,
       caretPadding: 2,
       cornerRadius: 6,
       backgroundColor: 'rgba(0,0,0,0.8)',
       multiKeyBackground: '#fff',
-    }));
+    });
 
     expect(tooltip).toEqual(jasmine.objectContaining({
       opacity: 1,
@@ -470,7 +470,6 @@ describe('Plugin.Tooltip', function() {
     expect(tooltip.y).toBeCloseToPixel(75);
   });
 
-
   it('Should provide context object to user callbacks', async function() {
     const chart = window.acquireChart({
       type: 'line',
@@ -811,10 +810,10 @@ describe('Plugin.Tooltip', function() {
     // Check and see if tooltip was displayed
     var tooltip = chart.tooltip;
 
-    expect(tooltip.options).toEqual(jasmine.objectContaining({
+    expect(tooltip.options).toEqualOptions({
       // Positioning
       caretPadding: 10,
-    }));
+    });
   });
 
   ['line', 'bar'].forEach(function(type) {
@@ -1184,51 +1183,51 @@ describe('Plugin.Tooltip', function() {
     expect(tooltip.xAlign).toEqual('center');
     expect(tooltip.yAlign).toEqual('top');
 
-    expect(tooltip.options.bodyFont).toEqual(jasmine.objectContaining({
+    expect(tooltip.options.bodyFont).toEqualOptions({
       family: defaults.font.family,
       style: defaults.font.style,
       size: defaults.font.size,
-    }));
+    });
 
-    expect(tooltip.options).toEqual(jasmine.objectContaining({
+    expect(tooltip.options).toEqualOptions({
       bodyAlign: 'left',
       bodySpacing: 2,
-    }));
+    });
 
-    expect(tooltip.options.titleFont).toEqual(jasmine.objectContaining({
+    expect(tooltip.options.titleFont).toEqualOptions({
       family: defaults.font.family,
       style: 'bold',
       size: defaults.font.size,
-    }));
+    });
 
-    expect(tooltip.options).toEqual(jasmine.objectContaining({
+    expect(tooltip.options).toEqualOptions({
       titleAlign: 'left',
       titleSpacing: 2,
       titleMarginBottom: 6,
-    }));
+    });
 
-    expect(tooltip.options.footerFont).toEqual(jasmine.objectContaining({
+    expect(tooltip.options.footerFont).toEqualOptions({
       family: defaults.font.family,
       style: 'bold',
       size: defaults.font.size,
-    }));
+    });
 
-    expect(tooltip.options).toEqual(jasmine.objectContaining({
+    expect(tooltip.options).toEqualOptions({
       footerAlign: 'left',
       footerSpacing: 2,
       footerMarginTop: 6,
-    }));
+    });
 
-    expect(tooltip.options).toEqual(jasmine.objectContaining({
+    expect(tooltip.options).toEqualOptions({
       // Appearance
       caretSize: 5,
       caretPadding: 2,
       cornerRadius: 6,
       backgroundColor: 'rgba(0,0,0,0.8)',
       multiKeyBackground: '#fff',
-    }));
+    });
 
-    expect(tooltip).toEqual(jasmine.objectContaining({
+    expect(tooltip).toEqualOptions({
       opacity: 1,
 
       // Text
@@ -1253,7 +1252,7 @@ describe('Plugin.Tooltip', function() {
         borderColor: defaults.borderColor,
         backgroundColor: defaults.backgroundColor
       }]
-    }));
+    });
   });
 
   describe('text align', function() {
index 847d3a2ed1abe46e5e573dc7aa8e4731389297ee..5cde43d05833d41a34750fc9954e20d6f788eb40 100644 (file)
@@ -546,7 +546,7 @@ export class DatasetController<TElement extends Element = Element, TDatasetEleme
        buildOrUpdateElements(resetNewElements?: boolean): void;
 
        getStyle(index: number, active: boolean): AnyObject;
-       protected resolveDatasetElementOptions(active: boolean): AnyObject;
+       protected resolveDatasetElementOptions(mode: UpdateMode): AnyObject;
        protected resolveDataElementOptions(index: number, mode: UpdateMode): AnyObject;
        /**
         * Utility for checking if the options are shared and should be animated separately.
@@ -592,8 +592,6 @@ export interface DatasetControllerChartComponent extends ChartComponent {
        defaults: {
                datasetElementType?: string | null | false;
                dataElementType?: string | null | false;
-               dataElementOptions?: string[];
-               datasetElementOptions?: string[] | { [key: string]: string };
        };
 }