Option resolution with proxies (#8374)

* Option resolution with proxies

* Remove plugin fallback to root options/defaults

* Update core plugins, reduntant font fallbacks

* Add some notes
This commit is contained in:
Jukka Kurkela 2021-02-15 21:42:32 +02:00 committed by GitHub
parent e1f254fc3e
commit cfd9c98575
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1220 additions and 786 deletions

View 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.

View 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

View 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

View File

@ -62,29 +62,23 @@
};
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: {

View File

@ -62,16 +62,13 @@
}]
},
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

View 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'
},

View 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']

View 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: {

View 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'

View 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
}
};

View 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',

View 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,
datasets: {
showLine: true,
},
elements: {
line: {
fill: 'start'
}
},
indexAxis: 'r',
scales: {
r: {
type: 'radialLinear',
}
},
indexAxis: 'r',
elements: {
line: {
fill: 'start',
tension: 0 // no bezier in radar
}
}
};

View 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: {}});

View 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.plugins = valueOrDefault(options.plugins, {});
options.scales = mergeScaleConfig(config, options);
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;
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;
}

View 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);

View 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 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);
const values = me._resolveOptions(me.dataElementOptions, {
index,
active,
info,
type: me.dataElementType.id
});
if (info.cacheable) {
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'
];

View 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,
}
});

View 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);

View 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);
}

View 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);
}
}

View 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) {

View File

@ -214,7 +214,8 @@ ArcElement.defaults = {
borderAlign: 'center',
borderColor: '#fff',
borderWidth: 2,
offset: 0
offset: 0,
angle: undefined
};
/**

View File

@ -256,7 +256,8 @@ BarElement.id = 'bar';
BarElement.defaults = {
borderSkipped: 'start',
borderWidth: 0,
borderRadius: 0
borderRadius: 0,
pointStyle: undefined
};
/**

View 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',
};

View File

@ -77,7 +77,8 @@ PointElement.defaults = {
hoverBorderWidth: 1,
hoverRadius: 4,
pointStyle: 'circle',
radius: 3
radius: 3,
rotation: 0
};
/**

View File

@ -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];
}

View 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';

View 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';

View 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

View 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;
}

View 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: ['']
};

View 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: ['']
};

View 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', '']
};

View 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'
}
};

View 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: {

View 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;

View 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() {},

View 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() {

View 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({

View 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() {

View File

@ -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');
});
});
});
});

View 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);
});
});

View 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);
});
});
});

View 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() {

View 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 };
};
}