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'
}
## 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.
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:
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);
}
```
* `dataset`: dataset at index `datasetIndex`
* `datasetIndex`: index of the current dataset
* `index`: getter for `datasetIndex`
+* `mode`: the update mode
* `type`: `'dataset'`
### data
* `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
* 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
};
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: {
}]
},
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
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,
};
if (includeOptions) {
- properties.options = options;
+ properties.options = sharedOptions || me.resolveDataElementOptions(i, mode);
}
me.updateElement(bars[i], i, properties, mode);
}
* 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);
/**
* @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);
BarController.defaults = {
datasetElementType: false,
dataElementType: 'bar',
- dataElementOptions: [
- 'backgroundColor',
- 'borderColor',
- 'borderSkipped',
- 'borderWidth',
- 'borderRadius',
- 'barPercentage',
- 'barThickness',
- 'base',
- 'categoryPercentage',
- 'maxBarThickness',
- 'minBarLength',
- 'pointStyle'
- ],
+
interaction: {
mode: 'index'
},
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() {
* @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;
}
BubbleController.defaults = {
datasetElementType: false,
dataElementType: 'point',
- dataElementOptions: [
- 'backgroundColor',
- 'borderColor',
- 'borderWidth',
- 'hitRadius',
- 'radius',
- 'pointStyle',
- 'rotation'
- ],
animation: {
numbers: {
properties: ['x', 'y', 'borderWidth', 'radius']
* @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);
}
/**
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.
*/
_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;
DoughnutController.defaults = {
datasetElementType: false,
dataElementType: 'arc',
- dataElementOptions: [
- 'backgroundColor',
- 'borderColor',
- 'borderWidth',
- 'borderAlign',
- 'offset'
- ],
animation: {
numbers: {
type: 'number',
},
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: {
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 {
// 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);
}
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);
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
*/
*/
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'
* @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
+ }
};
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.
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) {
outerRadius = 0;
}
if (animationOpts.animateRotate) {
- startAngle = datasetStartAngle;
- endAngle = datasetStartAngle;
+ startAngle = endAngle = datasetStartAngle;
}
}
/**
* @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;
}
}
*/
PolarAreaController.defaults = {
dataElementType: 'arc',
- dataElementOptions: [
- 'backgroundColor',
- 'borderColor',
- 'borderWidth',
- 'borderAlign',
- 'offset'
- ],
-
animation: {
numbers: {
type: 'number',
import DatasetController from '../core/core.datasetController';
-import {valueOrDefault} from '../helpers/helpers.core';
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);
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';
*/
RadarController.defaults = {
datasetElementType: 'line',
- datasetElementOptions: [
- 'backgroundColor',
- 'borderColor',
- 'borderCapStyle',
- 'borderDash',
- 'borderDashOffset',
- 'borderJoinStyle',
- 'borderWidth',
- 'fill'
- ],
-
dataElementType: 'point',
- dataElementOptions: {
- backgroundColor: 'pointBackgroundColor',
- borderColor: 'pointBorderColor',
- borderWidth: 'pointBorderWidth',
- hitRadius: 'pointHitRadius',
- hoverBackgroundColor: 'pointHoverBackgroundColor',
- hoverBorderColor: 'pointHoverBorderColor',
- hoverBorderWidth: 'pointHoverBorderWidth',
- hoverRadius: 'pointHoverRadius',
- pointStyle: 'pointStyle',
- radius: 'pointRadius',
- rotation: 'pointRotation'
- },
-
aspectRatio: 1,
- spanGaps: false,
- scales: {
- r: {
- type: 'radialLinear',
- }
+ datasets: {
+ showLine: true,
},
- indexAxis: 'r',
elements: {
line: {
- fill: 'start',
- tension: 0 // no bezier in radar
+ fill: 'start'
+ }
+ },
+ indexAxis: 'r',
+ scales: {
+ r: {
+ type: 'radialLinear',
}
}
};
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: {
}
});
-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) {
}
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);
}
});
});
}
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(() => {
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;
}
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: {}});
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] || {};
return scales;
}
-/**
- * Recursively merge the given config objects as the root options by handling
- * default scale options for the `scales` and `scale` properties, then returns
- * a deep copy of the result, thus doesn't alter inputs.
- */
-function mergeConfig(...args/* config objects ... */) {
- return merge(Object.create(null), args, {
- merger(key, target, source, options) {
- if (key !== 'scales' && key !== 'scale' && key !== 'controllers') {
- _merger(key, target, source, options);
- }
- }
- });
-}
-
-function includePluginDefaults(options) {
- options.plugins = options.plugins || {};
- options.plugins.title = (options.plugins.title !== false) && merge(Object.create(null), [
- defaults.plugins.title,
- options.plugins.title
- ]);
-
- options.plugins.tooltip = (options.plugins.tooltip !== false) && merge(Object.create(null), [
- defaults.interaction,
- defaults.plugins.tooltip,
- options.interaction,
- options.plugins.tooltip
- ]);
-}
-
-function includeDefaults(config, options) {
+function initOptions(config, options) {
options = options || {};
- const scaleConfig = mergeScaleConfig(config, options);
- const hoverEanbled = options.interaction !== false && options.hover !== false;
-
- options = mergeConfig(
- defaults,
- defaults.controllers[config.type],
- options);
-
- options.hover = hoverEanbled && merge(Object.create(null), [
- defaults.interaction,
- defaults.hover,
- options.interaction,
- options.hover
- ]);
-
- options.scales = scaleConfig;
+ options.plugins = valueOrDefault(options.plugins, {});
+ options.scales = mergeScaleConfig(config, options);
- if (options.plugins !== false) {
- includePluginDefaults(options);
- }
return options;
}
data.datasets = data.datasets || [];
data.labels = data.labels || [];
- config.options = includeDefaults(config, config.options);
+ config.options = initOptions(config, config.options);
return 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;
}
);
}
+ 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;
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 = [];
this.config.data = data;
}
+ get options() {
+ return this._options;
+ }
+
+ set options(options) {
+ this.config.update(options);
+ }
+
/**
* @private
*/
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);
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();
*/
_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);
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';
/**
});
}
-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);
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;
*/
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;
}
/**
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())));
}
/**
}
}
- /**
- * @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
* @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;
}
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);
}
/**
* @protected
*/
resolveDataElementOptions(index, mode) {
- mode = mode || 'default';
+ return this._resolveElementOptions(this.dataElementType.id, mode, index);
+ }
+
+ /**
+ * @private
+ */
+ _resolveElementOptions(elementType, mode = 'default', index) {
const me = this;
const active = mode === 'active';
const cache = me._cachedDataOpts;
- const cached = cache[mode];
- const sharing = me.enableOptionSharing;
+ const cacheKey = elementType + '-' + mode;
+ const cached = cache[cacheKey];
+ const sharing = me.enableOptionSharing && defined(index);
if (cached) {
return cloneIfNotShared(cached, sharing);
}
- const info = {cacheable: !active};
-
- const values = me._resolveOptions(me.dataElementOptions, {
- index,
- active,
- info,
- type: me.dataElementType.id
- });
-
- if (info.cacheable) {
+ const config = me.chart.config;
+ const scopeKeys = config.datasetElementScopeKeys(me._type, elementType);
+ const prefixes = active ? [`${elementType}Hover`, 'hover', elementType, ''] : [elementType, ''];
+ const scopes = config.getOptionScopes(me.getDataset(), scopeKeys);
+ const names = Object.keys(defaults.elements[elementType]);
+ // context is provided as a function, and is called only if needed,
+ // so we don't create a context for each element if not needed.
+ const context = () => me.getContext(index, active);
+ const values = config.resolveNamedOptions(scopes, names, context, prefixes);
+
+ if (values.$shared) {
// `$shared` indicates this set of options can be shared between multiple elements.
// Sharing is used to reduce number of properties to change during animation.
values.$shared = sharing;
// 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
_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;
}
*/
updateSharedOptions(sharedOptions, mode, newOptions) {
if (sharedOptions) {
- this._resolveAnimations(undefined, mode).update({options: sharedOptions}, {options: newOptions});
+ this._resolveAnimations(undefined, mode).update(sharedOptions, newOptions);
}
}
_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) {
* 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'
-];
+import {getHoverColor} from '../helpers/helpers.color';
import {isObject, merge, valueOrDefault} from '../helpers/helpers.core';
+const privateSymbol = Symbol('private');
+
/**
* @param {object} node
* @param {string} 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',
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
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);
}
/**
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.
}
// 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,
+ }
+});
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
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);
-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
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);
}
/**
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];
}
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);
+}
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';
/**
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
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
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);
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);
}
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);
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;
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;
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);
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;
* @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);
}
}
if (item.defaultRoutes) {
routeDefaults(scope, item.defaultRoutes);
}
+
+ if (item.descriptors) {
+ defaults.describe(scope, item.descriptors);
+ }
}
function routeDefaults(scope, routes) {
borderAlign: 'center',
borderColor: '#fff',
borderWidth: 2,
- offset: 0
+ offset: 0,
+ angle: undefined
};
/**
BarElement.defaults = {
borderSkipped: 'start',
borderWidth: 0,
- borderRadius: 0
+ borderRadius: 0,
+ pointStyle: undefined
};
/**
borderJoinStyle: 'miter',
borderWidth: 3,
capBezierPoints: true,
+ cubicInterpolationMode: 'default',
fill: false,
- tension: 0
+ spanGaps: false,
+ stepped: false,
+ tension: 0,
};
/**
backgroundColor: 'backgroundColor',
borderColor: 'borderColor'
};
+
+
+LineElement.descriptors = {
+ _scriptable: true,
+ _indexable: (name) => name !== 'borderDash' && name !== 'fill',
+};
hoverBorderWidth: 1,
hoverRadius: 4,
pointStyle: 'circle',
- radius: 3
+ radius: 3,
+ rotation: 0
};
/**
--- /dev/null
+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];
+}
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';
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';
* 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
* 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
// 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) {
/**
* @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
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;
}
}
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);
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;
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) {
*/
_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;
}
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: ['']
};
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;
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);
defaultRoutes: {
color: 'color'
- }
+ },
+
+ // For easier configuration, resolve additionally from root of options and defaults.
+ additionalOptionScopes: ['']
};
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';
}
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;
}
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)',
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', '']
};
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;
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;
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(
pointLabelPosition.y + (plFont.lineHeight / 2),
plFont,
{
- color: resolve([pointLabelOpts.color], context, i),
+ color: optsAtIndex.color,
textAlign: getTextAlignForAngle(angle),
}
);
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) {
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) {
this.pointLabels = [];
}
- init(options) {
- super.init(options);
- this.axis = 'r';
- }
-
setDimensions() {
const me = this;
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);
}
});
}
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;
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);
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,
});
});
'pointLabels.color': 'color',
'ticks.color': 'color'
};
+
+RadialLinearScale.descriptors = {
+ angleLines: {
+ _fallback: 'gridLines'
+ }
+};
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: {
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;
}
});
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() {
});
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};
}, 50);
});
-
it('should assign final shared options to target after animations complete', function(done) {
const chart = {
draw: 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');
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');
});
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');
options: {
responsive: true,
scales: {
- y: {
+ yAxis0: {
min: 0,
max: 10
}
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() {
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({
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() {
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);
});
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);
});
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);
});
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);
});
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);
});
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();
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() {
--- /dev/null
+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');
+ });
+ });
+ });
+});
lineWidth: 5,
strokeStyle: 'green',
pointStyle: 'crossRot',
- rotation: undefined,
+ rotation: 0,
datasetIndex: 0
}, {
text: 'dataset2',
lineWidth: 5,
strokeStyle: 'green',
pointStyle: 'star',
- rotation: undefined,
+ rotation: 0,
datasetIndex: 0
}, {
text: 'dataset2',
});
describe('config update', function() {
- it ('should update the options', function() {
+ it('should update the options', function() {
var chart = acquireChart({
type: 'line',
data: {
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: {},
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: {
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: {
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);
});
});
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);
});
});
});
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,
backgroundColor: 'rgba(0,0,0,0.8)',
multiKeyBackground: '#fff',
displayColors: true
- }));
+ });
expect(tooltip).toEqual(jasmine.objectContaining({
opacity: 1,
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,
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,
backgroundColor: 'rgba(0,0,0,0.8)',
multiKeyBackground: '#fff',
displayColors: true
- }));
+ });
expect(tooltip.opacity).toEqual(1);
expect(tooltip.title).toEqual(['Point 2']);
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,
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,
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,
expect(tooltip.y).toBeCloseToPixel(75);
});
-
it('Should provide context object to user callbacks', async function() {
const chart = window.acquireChart({
type: 'line',
// 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) {
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
borderColor: defaults.borderColor,
backgroundColor: defaults.backgroundColor
}]
- }));
+ });
});
describe('text align', function() {
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.
defaults: {
datasetElementType?: string | null | false;
dataElementType?: string | null | false;
- dataElementOptions?: string[];
- datasetElementOptions?: string[] | { [key: string]: string };
};
}