## Title Configuration
-The title configuration is passed into the `options.title` namespace. The global options for the chart title is defined in `Chart.defaults.title`.
+The title configuration is passed into the `options.title` namespace. The global options for the chart title is defined in `Chart.defaults.plugins.title`.
| Name | Type | Default | Description
| ---- | ---- | ------- | -----------
## Tooltip Configuration
-The tooltip configuration is passed into the `options.tooltips` namespace. The global options for the chart tooltips is defined in `Chart.defaults.tooltips`.
+The tooltip configuration is passed into the `options.tooltips` namespace. The global options for the chart tooltips is defined in `Chart.defaults.plugins.tooltip`.
| Name | Type | Default | Description
| ---- | ---- | ------- | -----------
* @param eventPosition {Point} the position of the event in canvas coordinates
* @returns {Point} the tooltip position
*/
-const tooltipPlugin = Chart.plugins.getAll().find(p => p.id === 'tooltip');
+const tooltipPlugin = Chart.registry.getPlugin('tooltip');
tooltipPlugin.positioners.custom = function(elements, eventPosition) {
/** @type {Tooltip} */
var tooltip = this;
Axes in Chart.js can be individually extended. Axes should always derive from `Chart.Scale` but this is not a mandatory requirement.
```javascript
-class MyScale extends Chart.Scale{
+class MyScale extends Chart.Scale {
/* extensions ... */
}
MyScale.id = 'myScale';
MyScale.defaults = defaultConfigObject;
+// Or in classic style
+/*
+function MyScale() {
+ Chart.Scale.call(this, arguments);
+ // constructor stuff
+}
+
+MyScale.prototype.draw = function(ctx) {
+ Chart.Scale.prototype.draw.call(this, arguments);
+ // ...
+}
+MyScale.id = 'myScale';
+MyScale.defaults = defaultConfigObject;
+*/
+
// MyScale is now derived from Chart.Scale
```
```javascript
Chart.register(MyScale);
+
+// If the scale is created in classical way, the prototype can not be used to detect what
+// you are trying to register - so you need to be explicit:
+
+// Chart.registry.addScales(MyScale);
```
To use the new scale, simply pass in the string key to the config when creating a chart.
```
## Scale Interface
+
To work with Chart.js, custom scale types must implement the following interface.
```javascript
```
The Core.Scale base class also has some utility functions that you may find useful.
+
```javascript
{
// Returns true if the scale instance is horizontal
options: options
});
```
+
+Same example in classic style
+
+```javascript
+function Custom() {
+ Chart.controllers.bubble.call(this, arguments);
+ // constructor stuff
+}
+
+Custom.prototype.draw = function(ctx) {
+ Chart.controllers.bubble.prototype.draw.call(this, arguments);
+
+ var meta = this.getMeta();
+ var pt0 = meta.data[0];
+ var radius = pt0.radius;
+
+ var ctx = this.chart.chart.ctx;
+ ctx.save();
+ ctx.strokeStyle = 'red';
+ ctx.lineWidth = 1;
+ ctx.strokeRect(pt0.x - radius, pt0.y - radius, 2 * radius, 2 * radius);
+ ctx.restore();}
+}
+
+Custom.id = 'derivedBubble';
+Custom.defaults = Chart.defaults.bubble;
+
+// Prototype chain can not be used to detect we are trying to register a controller, so we need
+// to be explicit
+Chart.registry.addControllers(Custom);
+
+// Now we can create and use our new chart type
+new Chart(ctx, {
+ type: 'derivedBubble',
+ data: data,
+ options: options
+});
+```
## Bundlers (Webpack, Rollup, etc.)
+Chart.js 3 is tree-shakeable, so it is necessary to import and register the controllers, elements, scales and plugins you are going to use.
+
```javascript
-import Chart from 'chart.js';
+import Chart, LineController, Line, Point, LinearScale, CategoryScale, Title, Tooltip, Filler, Legend from 'chart.js';
+
+Chart.register(LineController, Line, Point, LinearScale, CategoryScale, Title, Tooltip, Filler, Legend);
+
var myChart = new Chart(ctx, {...});
```
* API documentation generated and verified by TypeDoc
* No more CSS injection
* Tons of bug fixes
+* Tree shaking
## End user migration
* `Element._model` and `Element._view` are no longer used and properties are now set directly on the elements. You will have to use the method `getProps` to access these properties inside most methods such as `inXRange`/`inYRange` and `getCenterPoint`. Please take a look at [the Chart.js-provided elements](https://github.com/chartjs/Chart.js/tree/master/src/elements) for examples.
* When building the elements in a controller, it's now suggested to call `updateElement` to provide the element properties. There are also methods such as `getSharedOptions` and `includeOptions` that have been added to skip redundant computation. Please take a look at [the Chart.js-provided controllers](https://github.com/chartjs/Chart.js/tree/master/src/controllers) for examples.
* Scales introduced a new parsing API. This API takes user data and converts it into a more standard format. E.g. it allows users to provide numeric data as a `string` and converts it to a `number` where necessary. Previously this was done on the fly as charts were rendered. Now it's done up front with the ability to skip it for better performance if users provide data in the correct format. If you're using standard data format like `x`/`y` you may not need to do anything. If you're using a custom data format you will have to override some of the parse methods in `core.datasetController.js`. An example can be found in [chartjs-chart-financial](https://github.com/chartjs/chartjs-chart-financial), which uses an `{o, h, l, c}` data format.
+* Chart.js 3 is tree-shakeable. So when you use it as a module in a project, you need to import and register the controllers, elements, scales and plugins you want to use:
+
+```javascript
+import Chart, LineController, Line, Point, LinearScale, Title from `chart.js`
+
+Chart.register(LineController, Line, Point, LinearScale, Title);
+
+const chart = new Chart(ctx, {
+ type: 'line',
+ // data: ...
+ options: {
+ title: {
+ display: true,
+ text: 'Chart Title'
+ },
+ scales: {
+ x: {
+ type: 'linear'
+ },
+ y: {
+ type: 'linear'
+ }
+ }
+ }
+})
+```
A few changes were made to controllers that are more straight-forward, but will affect all controllers:
* `Chart.offsetX`
* `Chart.offsetY`
* `Chart.outerRadius` now lives on doughnut, pie, and polarArea controllers
+* `Chart.plugins` was replaced with `Chart.registry`. Plugin defaults are now in `Chart.defaults.plugins[id]`.
* `Chart.PolarArea`. New charts are created via `new Chart` and providing the appropriate `type` parameter
* `Chart.prototype.generateLegend`
* `Chart.platform`. It only contained `disableCSSInjection`. CSS is never injected in v3.
* `Chart.PluginBase`
* `Chart.Radar`. New charts are created via `new Chart` and providing the appropriate `type` parameter
* `Chart.radiusLength`
+* `Chart.scaleService` was replaced with `Chart.registry`. Scale defaults are now in `Chart.defaults.scales[type]`.
* `Chart.Scatter`. New charts are created via `new Chart` and providing the appropriate `type` parameter
* `Chart.types`
* `Chart.Title` was moved to `Chart.plugins.title._element` and made private
* `Scale.getLabelForIndex` was replaced by `scale.getLabelForValue`
* `Scale.getPixelForValue` now has only one parameter. For the `TimeScale` that parameter must be millis since the epoch
-* `ScaleService.registerScaleType` was renamed to `ScaleService.registerScale` and now takes a scale constructors which is expected to have `id` and `defaults` properties.
##### Ticks
It's easy to get started with Chart.js. All that's required is the script included in your page along with a single `<canvas>` node to render the chart.
In this example, we create a bar chart for a single dataset and render that in our page. You can see all the ways to use Chart.js in the [usage documentation](./getting-started/usage.md).
+
```html
<canvas id="myChart" width="400" height="400"></canvas>
<script>
"scripts": {
"autobuild": "rollup -c -w",
"build": "rollup -c",
- "dev": "cross-env NODE_ENV=test karma start ---auto-watch --no-single-run --browsers chrome --grep",
+ "dev": "karma start ---auto-watch --no-single-run --browsers chrome --grep",
"docs": "cd docs && npm install && npm run build && mkdir -p ../dist && cp -r build ../dist/docs",
"lint-js": "eslint samples/**/*.html samples/**/*.js src/**/*.js test/**/*.js",
"lint-tsc": "tsc",
</div>
<script>
- Chart.defaults.tooltips.custom = function(tooltip) {
+ Chart.defaults.plugins.tooltip.custom = function(tooltip) {
// Tooltip Element
var tooltipEl = document.getElementById('chartjs-tooltip');
import Interaction from './core.interaction';
import layouts from './core.layouts';
import {BasicPlatform, DomPlatform} from '../platform';
-import plugins from './core.plugins';
+import PluginService from './core.plugins';
import registry from './core.registry';
import {getMaximumWidth, getMaximumHeight, retinaScale} from '../helpers/helpers.dom';
import {mergeIf, merge, _merger, each, callback as callCallback, uid, valueOrDefault, _elementsEqual} from '../helpers/helpers.core';
const scaleConfig = mergeScaleConfig(config, config.options);
- config.options = mergeConfig(
+ const options = config.options = mergeConfig(
defaults,
defaults[config.type],
config.options || {});
- config.options.scales = scaleConfig;
+ options.scales = scaleConfig;
+
+ options.title = (options.title !== false) && merge({}, [defaults.plugins.title, options.title]);
+ options.tooltips = (options.tooltips !== false) && merge({}, [defaults.plugins.tooltip, options.tooltips]);
return config;
}
const chart = ctx.chart;
const animationOptions = chart.options.animation;
- plugins.notify(chart, 'afterRender');
+ chart._plugins.notify(chart, 'afterRender');
callCallback(animationOptions && animationOptions.onComplete, [ctx], chart);
}
this._updating = false;
this.scales = {};
this.scale = undefined;
- this.$plugins = undefined;
+ this._plugins = new PluginService();
this.$proxies = {};
this._hiddenIndices = {};
this.attached = false;
const me = this;
// Before init plugin notification
- plugins.notify(me, 'beforeInit');
+ me._plugins.notify(me, 'beforeInit');
if (me.options.responsive) {
// Initial resize before chart draws (must be silent to preserve initial animations).
me.bindEvents();
// After init plugin notification
- plugins.notify(me, 'afterInit');
+ me._plugins.notify(me, 'afterInit');
return me;
}
retinaScale(me, newRatio);
if (!silent) {
- plugins.notify(me, 'resize', [newSize]);
+ me._plugins.notify(me, 'resize', [newSize]);
callCallback(options.onResize, [newSize], me);
*/
reset() {
this._resetElements();
- plugins.notify(this, 'reset');
+ this._plugins.notify(this, 'reset');
}
update(mode) {
// plugins options references might have change, let's invalidate the cache
// https://github.com/chartjs/Chart.js/issues/5111#issuecomment-355934167
- plugins.invalidate(me);
+ me._plugins.invalidate();
- if (plugins.notify(me, 'beforeUpdate') === false) {
+ if (me._plugins.notify(me, 'beforeUpdate') === false) {
return;
}
me._updateDatasets(mode);
// Do this before render so that any plugins that need final scale updates can use it
- plugins.notify(me, 'afterUpdate');
+ me._plugins.notify(me, 'afterUpdate');
me._layers.sort(compare2Level('z', '_idx'));
_updateLayout() {
const me = this;
- if (plugins.notify(me, 'beforeLayout') === false) {
+ if (me._plugins.notify(me, 'beforeLayout') === false) {
return;
}
item._idx = index;
});
- plugins.notify(me, 'afterLayout');
+ me._plugins.notify(me, 'afterLayout');
}
/**
const me = this;
const isFunction = typeof mode === 'function';
- if (plugins.notify(me, 'beforeDatasetsUpdate') === false) {
+ if (me._plugins.notify(me, 'beforeDatasetsUpdate') === false) {
return;
}
me._updateDataset(i, isFunction ? mode({datasetIndex: i}) : mode);
}
- plugins.notify(me, 'afterDatasetsUpdate');
+ me._plugins.notify(me, 'afterDatasetsUpdate');
}
/**
const meta = me.getDatasetMeta(index);
const args = {meta, index, mode};
- if (plugins.notify(me, 'beforeDatasetUpdate', [args]) === false) {
+ if (me._plugins.notify(me, 'beforeDatasetUpdate', [args]) === false) {
return;
}
meta.controller._update(mode);
- plugins.notify(me, 'afterDatasetUpdate', [args]);
+ me._plugins.notify(me, 'afterDatasetUpdate', [args]);
}
render() {
const me = this;
const animationOptions = me.options.animation;
- if (plugins.notify(me, 'beforeRender') === false) {
+ if (me._plugins.notify(me, 'beforeRender') === false) {
return;
}
const onComplete = function() {
- plugins.notify(me, 'afterRender');
+ me._plugins.notify(me, 'afterRender');
callCallback(animationOptions && animationOptions.onComplete, [], me);
};
return;
}
- if (plugins.notify(me, 'beforeDraw') === false) {
+ if (me._plugins.notify(me, 'beforeDraw') === false) {
return;
}
layers[i].draw(me.chartArea);
}
- plugins.notify(me, 'afterDraw');
+ me._plugins.notify(me, 'afterDraw');
}
/**
_drawDatasets() {
const me = this;
- if (plugins.notify(me, 'beforeDatasetsDraw') === false) {
+ if (me._plugins.notify(me, 'beforeDatasetsDraw') === false) {
return;
}
me._drawDataset(metasets[i]);
}
- plugins.notify(me, 'afterDatasetsDraw');
+ me._plugins.notify(me, 'afterDatasetsDraw');
}
/**
index: meta.index,
};
- if (plugins.notify(me, 'beforeDatasetDraw', [args]) === false) {
+ if (me._plugins.notify(me, 'beforeDatasetDraw', [args]) === false) {
return;
}
unclipArea(ctx);
- plugins.notify(me, 'afterDatasetDraw', [args]);
+ me._plugins.notify(me, 'afterDatasetDraw', [args]);
}
/**
me.ctx = null;
}
- plugins.notify(me, 'destroy');
+ me._plugins.notify(me, 'destroy');
delete Chart.instances[me.id];
}
_eventHandler(e, replay) {
const me = this;
- if (plugins.notify(me, 'beforeEvent', [e, replay]) === false) {
+ if (me._plugins.notify(me, 'beforeEvent', [e, replay]) === false) {
return;
}
me._handleEvent(e, replay);
- plugins.notify(me, 'afterEvent', [e, replay]);
+ me._plugins.notify(me, 'afterEvent', [e, replay]);
me.render();
import Animations from './core.animations';
-import {isObject, merge, _merger, isArray, valueOrDefault, mergeIf, resolveObjectKey} from '../helpers/helpers.core';
+import {isObject, merge, _merger, isArray, valueOrDefault, mergeIf, resolveObjectKey, _capitalize} from '../helpers/helpers.core';
import {listenArrayEvents, unlistenArrayEvents} from '../helpers/helpers.collection';
import {resolve} from '../helpers/helpers.options';
import {getHoverColor} from '../helpers/helpers.color';
}
function optionKey(key, active) {
- return active ? 'hover' + key.charAt(0).toUpperCase() + key.slice(1) : key;
+ return active ? 'hover' + _capitalize(key) : key;
}
export default class DatasetController {
this.onClick = null;
this.responsive = true;
this.showLines = true;
- this.plugins = undefined;
+ this.plugins = {};
this.scale = undefined;
this.legend = undefined;
this.title = undefined;
import defaults from './core.defaults';
-import {clone} from '../helpers/helpers.core';
+import registry from './core.registry';
+import {mergeIf} from '../helpers/helpers.core';
/**
* @typedef { import("./core.controller").default } Chart
* @typedef { import("../plugins/plugin.tooltip").default } Tooltip
*/
-defaults.set('plugins', {});
-
-/**
- * The plugin service singleton
- * @namespace Chart.plugins
- * @since 2.1.0
- */
-export class PluginService {
- constructor() {
- /**
- * Globally registered plugins.
- * @private
- */
- this._plugins = [];
-
- /**
- * This identifier is used to invalidate the descriptors cache attached to each chart
- * when a global plugin is registered or unregistered. In this case, the cache ID is
- * incremented and descriptors are regenerated during following API calls.
- * @private
- */
- this._cacheId = 0;
- }
-
- /**
- * Registers the given plugin(s) if not already registered.
- * @param {IPlugin[]|IPlugin} plugins plugin instance(s).
- */
- register(plugins) {
- const p = this._plugins;
- ([]).concat(plugins).forEach((plugin) => {
- if (p.indexOf(plugin) === -1) {
- p.push(plugin);
- }
- });
-
- this._cacheId++;
- }
-
- /**
- * Unregisters the given plugin(s) only if registered.
- * @param {IPlugin[]|IPlugin} plugins plugin instance(s).
- */
- unregister(plugins) {
- const p = this._plugins;
- ([]).concat(plugins).forEach((plugin) => {
- const idx = p.indexOf(plugin);
- if (idx !== -1) {
- p.splice(idx, 1);
- }
- });
-
- this._cacheId++;
- }
-
- /**
- * Remove all registered plugins.
- * @since 2.1.5
- */
- clear() {
- this._plugins = [];
- this._cacheId++;
- }
-
- /**
- * Returns the number of registered plugins?
- * @returns {number}
- * @since 2.1.5
- */
- count() {
- return this._plugins.length;
- }
-
- /**
- * Returns all registered plugin instances.
- * @returns {IPlugin[]} array of plugin objects.
- * @since 2.1.5
- */
- getAll() {
- return this._plugins;
- }
-
+export default class PluginService {
/**
* Calls enabled plugins for `chart` on the specified hook and with the given args.
* This method immediately returns as soon as a plugin explicitly returns false. The
*/
notify(chart, hook, args) {
const descriptors = this._descriptors(chart);
- const ilen = descriptors.length;
- let i, descriptor, plugin, params, method;
- for (i = 0; i < ilen; ++i) {
- descriptor = descriptors[i];
- plugin = descriptor.plugin;
- method = plugin[hook];
+ for (let i = 0; i < descriptors.length; ++i) {
+ const descriptor = descriptors[i];
+ const plugin = descriptor.plugin;
+ const method = plugin[hook];
if (typeof method === 'function') {
- params = [chart].concat(args || []);
+ const params = [chart].concat(args || []);
params.push(descriptor.options);
if (method.apply(plugin, params) === false) {
return false;
return true;
}
+ invalidate() {
+ this._cache = undefined;
+ }
+
/**
- * Returns descriptors of enabled plugins for the given chart.
* @param {Chart} chart
- * @returns {object[]} [{ plugin, options }]
* @private
*/
_descriptors(chart) {
- const cache = chart.$plugins || (chart.$plugins = {});
- if (cache.id === this._cacheId) {
- return cache.descriptors;
+ if (this._cache) {
+ return this._cache;
}
- const plugins = [];
- const descriptors = [];
const config = (chart && chart.config) || {};
const options = (config.options && config.options.plugins) || {};
+ const plugins = allPlugins(config);
+ const descriptors = createDescriptors(plugins, options);
- this._plugins.concat(config.plugins || []).forEach((plugin) => {
- const idx = plugins.indexOf(plugin);
- if (idx !== -1) {
- return;
- }
+ this._cache = descriptors;
- const id = plugin.id;
- let opts = options[id];
- if (opts === false) {
- return;
- }
+ return descriptors;
+ }
+}
- if (opts === true) {
- opts = clone(defaults.plugins[id]);
- }
+function allPlugins(config) {
+ const plugins = [];
+ const keys = Object.keys(registry.plugins.items);
+ for (let i = 0; i < keys.length; i++) {
+ plugins.push(registry.getPlugin(keys[i]));
+ }
- plugins.push(plugin);
- descriptors.push({
- plugin,
- options: opts || {}
- });
- });
+ const local = config.plugins || [];
+ for (let i = 0; i < local.length; i++) {
+ const plugin = local[i];
- cache.descriptors = descriptors;
- cache.id = this._cacheId;
- return descriptors;
+ if (plugins.indexOf(plugin) === -1) {
+ plugins.push(plugin);
+ }
}
- /**
- * Invalidates cache for the given chart: descriptors hold a reference on plugin option,
- * but in some cases, this reference can be changed by the user when updating options.
- * https://github.com/chartjs/Chart.js/issues/5111#issuecomment-355934167
- * @param {Chart} chart
- */
- invalidate(chart) {
- delete chart.$plugins;
- }
+ return plugins;
}
-// singleton instance
-export default new PluginService();
+function createDescriptors(plugins, options) {
+ const result = [];
+
+ for (let i = 0; i < plugins.length; i++) {
+ const plugin = plugins[i];
+ const id = plugin.id;
+
+ let opts = options[id];
+ if (opts === false) {
+ continue;
+ }
+ if (opts === true) {
+ opts = {};
+ }
+ result.push({
+ plugin,
+ options: mergeIf({}, [opts, defaults.plugins[id]])
+ });
+ }
+
+ return result;
+}
/**
* Plugin extension hooks.
import Element from './core.element';
import Scale from './core.scale';
import TypedRegistry from './core.typedRegistry';
-import {each, callback as call} from '../helpers/helpers.core';
+import {each, callback as call, _capitalize} from '../helpers/helpers.core';
/**
* Please use the module's default export which provides a singleton instance
* @param {...any} args
*/
add(...args) {
- this._registerEach(args);
+ this._each('register', args);
+ }
+
+ remove(...args) {
+ this._each('unregister', args);
}
/**
* @param {...typeof DatasetController} args
*/
addControllers(...args) {
- this._registerEach(args, this.controllers);
+ this._each('register', args, this.controllers);
}
/**
* @param {...typeof Element} args
*/
addElements(...args) {
- this._registerEach(args, this.elements);
+ this._each('register', args, this.elements);
}
/**
* @param {...any} args
*/
addPlugins(...args) {
- this._registerEach(args, this.plugins);
+ this._each('register', args, this.plugins);
}
/**
* @param {...typeof Scale} args
*/
addScales(...args) {
- this._registerEach(args, this.scales);
+ this._each('register', args, this.scales);
}
/**
/**
* @private
*/
- _registerEach(args, typedRegistry) {
+ _each(method, args, typedRegistry) {
const me = this;
[...args].forEach(arg => {
const reg = typedRegistry || me._getRegistryForType(arg);
- if (reg.isForType(arg)) {
- me._registerComponent(reg, arg);
+ if (reg.isForType(arg) || (reg === me.plugins && arg.id)) {
+ me._exec(method, reg, arg);
} else {
// Handle loopable args
// Use case:
// Chart.register(treemap);
const itemReg = typedRegistry || me._getRegistryForType(item);
- me._registerComponent(itemReg, item);
+ me._exec(method, itemReg, item);
});
}
});
/**
* @private
*/
- _registerComponent(registry, component) {
- call(component.beforeRegister, [], component);
- registry.register(component);
- call(component.afterRegister, [], component);
+ _exec(method, registry, component) {
+ const camelMethod = _capitalize(method);
+ call(component['before' + camelMethod], [], component);
+ registry[method](component);
+ call(component['after' + camelMethod], [], component);
}
/**
+import {_capitalize} from './helpers.core';
+
/**
* Binary search
* @param {array} table - the table search. must be sorted!
});
arrayEvents.forEach((key) => {
- const method = '_onData' + key.charAt(0).toUpperCase() + key.slice(1);
+ const method = '_onData' + _capitalize(key);
const base = array[key];
Object.defineProperty(array, key, {
}
return obj;
}
+
+/**
+ * @private
+ */
+export function _capitalize(str) {
+ return str.charAt(0).toUpperCase() + str.slice(1);
+}
import layouts from './core/core.layouts';
import * as platforms from './platform/index';
import * as plugins from './plugins';
-import pluginsCore from './core/core.plugins';
import registry from './core/core.registry';
import Scale from './core/core.scale';
import * as scales from './scales';
import Ticks from './core/core.ticks';
+import {each} from './helpers/helpers.core';
-Chart.register = (...items) => registry.add(...items);
+// @ts-ignore
+const invalidatePlugins = () => each(Chart.instances, (chart) => chart._plugins.invalidate());
+
+Chart.register = (...items) => {
+ registry.add(...items);
+ invalidatePlugins();
+};
+Chart.unregister = (...items) => {
+ registry.remove(...items);
+ invalidatePlugins();
+};
// Register built-ins
Chart.register(controllers, scales, elements, plugins);
Chart.Interaction = Interaction;
Chart.layouts = layouts;
Chart.platforms = platforms;
-Chart.plugins = pluginsCore;
Chart.registry = registry;
Chart.Scale = Scale;
Chart.Ticks = Ticks;
-for (const k in plugins) {
- if (Object.prototype.hasOwnProperty.call(plugins, k)) {
- Chart.plugins.register(plugins[k]);
- }
-}
-
if (typeof window !== 'undefined') {
// @ts-ignore
window.Chart = Chart;
-export {default as filler} from './plugin.filler';
-export {default as legend} from './plugin.legend';
-export {default as title} from './plugin.title';
-export {default as tooltip} from './plugin.tooltip';
+export {default as Filler} from './plugin.filler';
+export {default as Legend} from './plugin.legend';
+export {default as Title} from './plugin.title';
+export {default as Tooltip} from './plugin.tooltip';
import {isArray, mergeIf} from '../helpers/helpers.core';
import {toPadding, toFont} from '../helpers/helpers.options';
-defaults.set('title', {
- align: 'center',
- display: false,
- font: {
- style: 'bold',
- },
- fullWidth: true,
- padding: 10,
- position: 'top',
- text: '',
- weight: 2000 // by default greater than legend (1000) to be above
-});
-
export class Title extends Element {
constructor(config) {
super();
const titleBlock = chart.titleBlock;
if (titleOpts) {
- mergeIf(titleOpts, defaults.title);
+ mergeIf(titleOpts, defaults.plugins.title);
if (titleBlock) {
layouts.configure(chart, titleBlock, titleOpts);
layouts.removeBox(chart, titleBlock);
delete chart.titleBlock;
}
+ },
+
+ defaults: {
+ align: 'center',
+ display: false,
+ font: {
+ style: 'bold',
+ },
+ fullWidth: true,
+ padding: 10,
+ position: 'top',
+ text: '',
+ weight: 2000 // by default greater than legend (1000) to be above
}
};
import Animations from '../core/core.animations';
import defaults from '../core/core.defaults';
import Element from '../core/core.element';
-import plugins from '../core/core.plugins';
-import {valueOrDefault, each, noop, isNullOrUndef, isArray, _elementsEqual} from '../helpers/helpers.core';
+import {valueOrDefault, each, noop, isNullOrUndef, isArray, _elementsEqual, merge} from '../helpers/helpers.core';
import {getRtlAdapter, overrideTextDirection, restoreTextDirection} from '../helpers/helpers.rtl';
import {distanceBetweenPoints} from '../helpers/helpers.math';
import {toFont} from '../helpers/helpers.options';
* @typedef { import("../platform/platform.base").IEvent } IEvent
*/
-defaults.set('tooltips', {
- enabled: true,
- custom: null,
- mode: 'nearest',
- position: 'average',
- intersect: true,
- backgroundColor: 'rgba(0,0,0,0.8)',
- titleFont: {
- style: 'bold',
- color: '#fff',
- },
- titleSpacing: 2,
- titleMarginBottom: 6,
- titleAlign: 'left',
- bodySpacing: 2,
- bodyFont: {
- color: '#fff',
- },
- bodyAlign: 'left',
- footerSpacing: 2,
- footerMarginTop: 6,
- footerFont: {
- color: '#fff',
- style: 'bold',
- },
- footerAlign: 'left',
- yPadding: 6,
- xPadding: 6,
- caretPadding: 2,
- caretSize: 5,
- cornerRadius: 6,
- multiKeyBackground: '#fff',
- displayColors: true,
- borderColor: 'rgba(0,0,0,0)',
- borderWidth: 0,
- animation: {
- duration: 400,
- easing: 'easeOutQuart',
- numbers: {
- type: 'number',
- properties: ['x', 'y', 'width', 'height', 'caretX', 'caretY'],
- },
- opacity: {
- easing: 'linear',
- duration: 200
- }
- },
- callbacks: {
- // Args are: (tooltipItems, data)
- beforeTitle: noop,
- title(tooltipItems, data) {
- let title = '';
- const labels = data.labels;
- const labelCount = labels ? labels.length : 0;
-
- if (tooltipItems.length > 0) {
- const item = tooltipItems[0];
- if (item.label) {
- title = item.label;
- } else if (labelCount > 0 && item.index < labelCount) {
- title = labels[item.index];
- }
- }
-
- return title;
- },
- afterTitle: noop,
-
- // Args are: (tooltipItems, data)
- beforeBody: noop,
-
- // Args are: (tooltipItem, data)
- beforeLabel: noop,
- label(tooltipItem, data) {
- let label = data.datasets[tooltipItem.datasetIndex].label || '';
-
- if (label) {
- label += ': ';
- }
- const value = tooltipItem.value;
- if (!isNullOrUndef(value)) {
- label += value;
- }
- return label;
- },
- labelColor(tooltipItem, chart) {
- const meta = chart.getDatasetMeta(tooltipItem.datasetIndex);
- const options = meta.controller.getStyle(tooltipItem.index);
- return {
- borderColor: options.borderColor,
- backgroundColor: options.backgroundColor
- };
- },
- labelTextColor() {
- return this.options.bodyFont.color;
- },
- afterLabel: noop,
-
- // Args are: (tooltipItems, data)
- afterBody: noop,
-
- // Args are: (tooltipItems, data)
- beforeFooter: noop,
- footer: noop,
- afterFooter: noop
- }
-});
-
const positioners = {
/**
* Average mode places the tooltip at the average position of the elements shown
*/
function resolveOptions(options) {
- options = Object.assign({}, defaults.tooltips, options);
+ options = merge({}, [defaults.plugins.tooltip, options]);
options.bodyFont = toFont(options.bodyFont);
options.titleFont = toFont(options.titleFont);
tooltip
};
- if (plugins.notify(chart, 'beforeTooltipDraw', [args]) === false) {
+ if (chart._plugins.notify(chart, 'beforeTooltipDraw', [args]) === false) {
return;
}
tooltip.draw(chart.ctx);
}
- plugins.notify(chart, 'afterTooltipDraw', [args]);
+ chart._plugins.notify(chart, 'afterTooltipDraw', [args]);
},
afterEvent(chart, e, replay) {
const useFinalPosition = replay;
chart.tooltip.handleEvent(e, useFinalPosition);
}
+ },
+
+ defaults: {
+ enabled: true,
+ custom: null,
+ mode: 'nearest',
+ position: 'average',
+ intersect: true,
+ backgroundColor: 'rgba(0,0,0,0.8)',
+ titleFont: {
+ style: 'bold',
+ color: '#fff',
+ },
+ titleSpacing: 2,
+ titleMarginBottom: 6,
+ titleAlign: 'left',
+ bodySpacing: 2,
+ bodyFont: {
+ color: '#fff',
+ },
+ bodyAlign: 'left',
+ footerSpacing: 2,
+ footerMarginTop: 6,
+ footerFont: {
+ color: '#fff',
+ style: 'bold',
+ },
+ footerAlign: 'left',
+ yPadding: 6,
+ xPadding: 6,
+ caretPadding: 2,
+ caretSize: 5,
+ cornerRadius: 6,
+ multiKeyBackground: '#fff',
+ displayColors: true,
+ borderColor: 'rgba(0,0,0,0)',
+ borderWidth: 0,
+ animation: {
+ duration: 400,
+ easing: 'easeOutQuart',
+ numbers: {
+ type: 'number',
+ properties: ['x', 'y', 'width', 'height', 'caretX', 'caretY'],
+ },
+ opacity: {
+ easing: 'linear',
+ duration: 200
+ }
+ },
+ callbacks: {
+ // Args are: (tooltipItems, data)
+ beforeTitle: noop,
+ title(tooltipItems, data) {
+ let title = '';
+ const labels = data.labels;
+ const labelCount = labels ? labels.length : 0;
+
+ if (tooltipItems.length > 0) {
+ const item = tooltipItems[0];
+ if (item.label) {
+ title = item.label;
+ } else if (labelCount > 0 && item.index < labelCount) {
+ title = labels[item.index];
+ }
+ }
+
+ return title;
+ },
+ afterTitle: noop,
+
+ // Args are: (tooltipItems, data)
+ beforeBody: noop,
+
+ // Args are: (tooltipItem, data)
+ beforeLabel: noop,
+ label(tooltipItem, data) {
+ let label = data.datasets[tooltipItem.datasetIndex].label || '';
+
+ if (label) {
+ label += ': ';
+ }
+ const value = tooltipItem.value;
+ if (!isNullOrUndef(value)) {
+ label += value;
+ }
+ return label;
+ },
+ labelColor(tooltipItem, chart) {
+ const meta = chart.getDatasetMeta(tooltipItem.datasetIndex);
+ const options = meta.controller.getStyle(tooltipItem.index);
+ return {
+ borderColor: options.borderColor,
+ backgroundColor: options.backgroundColor
+ };
+ },
+ labelTextColor() {
+ return this.options.bodyFont.color;
+ },
+ afterLabel: noop,
+
+ // Args are: (tooltipItems, data)
+ afterBody: noop,
+
+ // Args are: (tooltipItems, data)
+ beforeFooter: noop,
+ footer: noop,
+ afterFooter: noop
+ }
}
};
describe('Chart.plugins', function() {
- beforeEach(function() {
- this._plugins = Chart.plugins.getAll();
- Chart.plugins.clear();
- });
-
- afterEach(function() {
- Chart.plugins.clear();
- Chart.plugins.register(this._plugins);
- delete this._plugins;
- });
-
- describe('Chart.plugins.register', function() {
- it('should register a plugin', function() {
- Chart.plugins.register({});
- expect(Chart.plugins.count()).toBe(1);
- Chart.plugins.register({});
- expect(Chart.plugins.count()).toBe(2);
- });
-
- it('should register an array of plugins', function() {
- Chart.plugins.register([{}, {}, {}]);
- expect(Chart.plugins.count()).toBe(3);
- });
-
- it('should succeed to register an already registered plugin', function() {
- var plugin = {};
- Chart.plugins.register(plugin);
- expect(Chart.plugins.count()).toBe(1);
- Chart.plugins.register(plugin);
- expect(Chart.plugins.count()).toBe(1);
- Chart.plugins.register([{}, plugin, plugin]);
- expect(Chart.plugins.count()).toBe(2);
- });
- });
-
- describe('Chart.plugins.unregister', function() {
- it('should unregister a plugin', function() {
- var plugin = {};
- Chart.plugins.register(plugin);
- expect(Chart.plugins.count()).toBe(1);
- Chart.plugins.unregister(plugin);
- expect(Chart.plugins.count()).toBe(0);
- });
-
- it('should unregister an array of plugins', function() {
- var plugins = [{}, {}, {}];
- Chart.plugins.register(plugins);
- expect(Chart.plugins.count()).toBe(3);
- Chart.plugins.unregister(plugins.slice(0, 2));
- expect(Chart.plugins.count()).toBe(1);
- });
-
- it('should succeed to unregister a plugin not registered', function() {
- var plugin = {};
- Chart.plugins.register(plugin);
- expect(Chart.plugins.count()).toBe(1);
- Chart.plugins.unregister({});
- expect(Chart.plugins.count()).toBe(1);
- Chart.plugins.unregister([{}, plugin]);
- expect(Chart.plugins.count()).toBe(0);
- });
- });
-
describe('Chart.plugins.notify', function() {
it('should call inline plugins with arguments', function() {
var plugin = {hook: function() {}};
spyOn(plugin, 'hook');
- Chart.plugins.notify(chart, 'hook', 42);
+ chart._plugins.notify(chart, 'hook', 42);
expect(plugin.hook.calls.count()).toBe(1);
expect(plugin.hook.calls.first().args[0]).toBe(chart);
expect(plugin.hook.calls.first().args[1]).toBe(42);
});
it('should call global plugins with arguments', function() {
- var plugin = {hook: function() {}};
+ var plugin = {id: 'a', hook: function() {}};
var chart = window.acquireChart({});
spyOn(plugin, 'hook');
- Chart.plugins.register(plugin);
- Chart.plugins.notify(chart, 'hook', 42);
+ Chart.register(plugin);
+ chart._plugins.notify(chart, 'hook', 42);
expect(plugin.hook.calls.count()).toBe(1);
expect(plugin.hook.calls.first().args[0]).toBe(chart);
expect(plugin.hook.calls.first().args[1]).toBe(42);
expect(plugin.hook.calls.first().args[2]).toEqual({});
+ Chart.unregister(plugin);
});
it('should call plugin only once even if registered multiple times', function() {
- var plugin = {hook: function() {}};
+ var plugin = {id: 'test', hook: function() {}};
var chart = window.acquireChart({
plugins: [plugin, plugin]
});
spyOn(plugin, 'hook');
- Chart.plugins.register([plugin, plugin]);
- Chart.plugins.notify(chart, 'hook');
+ Chart.register([plugin, plugin]);
+ chart._plugins.notify(chart, 'hook');
expect(plugin.hook.calls.count()).toBe(1);
+ Chart.unregister(plugin);
});
it('should call plugins in the correct order (global first)', function() {
}]
});
- Chart.plugins.register([{
+ var plugins = [{
+ id: 'a',
hook: function() {
results.push(4);
}
}, {
+ id: 'b',
hook: function() {
results.push(5);
}
}, {
+ id: 'c',
hook: function() {
results.push(6);
}
- }]);
+ }];
+ Chart.register(plugins);
- var ret = Chart.plugins.notify(chart, 'hook');
+ var ret = chart._plugins.notify(chart, 'hook');
expect(ret).toBeTruthy();
expect(results).toEqual([4, 5, 6, 1, 2, 3]);
+ Chart.unregister(plugins);
});
it('should return TRUE if no plugin explicitly returns FALSE', function() {
spyOn(plugin, 'hook').and.callThrough();
});
- var ret = Chart.plugins.notify(chart, 'hook');
+ var ret = chart._plugins.notify(chart, 'hook');
expect(ret).toBeTruthy();
plugins.forEach(function(plugin) {
expect(plugin.hook).toHaveBeenCalled();
spyOn(plugin, 'hook').and.callThrough();
});
- var ret = Chart.plugins.notify(chart, 'hook');
+ var ret = chart._plugins.notify(chart, 'hook');
expect(ret).toBeFalsy();
expect(plugins[0].hook).toHaveBeenCalled();
expect(plugins[1].hook).toHaveBeenCalled();
describe('config.options.plugins', function() {
it('should call plugins with options at last argument', function() {
var plugin = {id: 'foo', hook: function() {}};
+
var chart = window.acquireChart({
options: {
plugins: {
spyOn(plugin, 'hook');
- Chart.plugins.register(plugin);
- Chart.plugins.notify(chart, 'hook');
- Chart.plugins.notify(chart, 'hook', ['bla']);
- Chart.plugins.notify(chart, 'hook', ['bla', 42]);
+ Chart.register(plugin);
+ chart._plugins.notify(chart, 'hook');
+ chart._plugins.notify(chart, 'hook', ['bla']);
+ chart._plugins.notify(chart, 'hook', ['bla', 42]);
expect(plugin.hook.calls.count()).toBe(3);
expect(plugin.hook.calls.argsFor(0)[1]).toEqual({a: '123'});
expect(plugin.hook.calls.argsFor(1)[2]).toEqual({a: '123'});
expect(plugin.hook.calls.argsFor(2)[3]).toEqual({a: '123'});
+
+ Chart.unregister(plugin);
});
it('should call plugins with options associated to their identifier', function() {
c: {id: 'c', hook: function() {}}
};
- Chart.plugins.register(plugins.a);
+ Chart.register(plugins.a);
var chart = window.acquireChart({
plugins: [plugins.b, plugins.c],
spyOn(plugins.b, 'hook');
spyOn(plugins.c, 'hook');
- Chart.plugins.notify(chart, 'hook');
+ chart._plugins.notify(chart, 'hook');
expect(plugins.a.hook).toHaveBeenCalled();
expect(plugins.b.hook).toHaveBeenCalled();
expect(plugins.a.hook.calls.first().args[1]).toEqual({a: '123'});
expect(plugins.b.hook.calls.first().args[1]).toEqual({b: '456'});
expect(plugins.c.hook.calls.first().args[1]).toEqual({c: '789'});
+
+ Chart.unregister(plugins.a);
});
it('should not called plugins when config.options.plugins.{id} is FALSE', function() {
c: {id: 'c', hook: function() {}}
};
- Chart.plugins.register(plugins.a);
+ Chart.register(plugins.a);
var chart = window.acquireChart({
plugins: [plugins.b, plugins.c],
spyOn(plugins.b, 'hook');
spyOn(plugins.c, 'hook');
- Chart.plugins.notify(chart, 'hook');
+ chart._plugins.notify(chart, 'hook');
expect(plugins.a.hook).not.toHaveBeenCalled();
expect(plugins.b.hook).not.toHaveBeenCalled();
expect(plugins.c.hook).toHaveBeenCalled();
+
+ Chart.unregister(plugins.a);
});
it('should call plugins with default options when plugin options is TRUE', function() {
- var plugin = {id: 'a', hook: function() {}};
+ var plugin = {id: 'a', hook: function() {}, defaults: {a: 42}};
- Chart.defaults.plugins.a = {a: 42};
- Chart.plugins.register(plugin);
+ Chart.register(plugin);
var chart = window.acquireChart({
options: {
spyOn(plugin, 'hook');
- Chart.plugins.notify(chart, 'hook');
+ chart._plugins.notify(chart, 'hook');
expect(plugin.hook).toHaveBeenCalled();
expect(plugin.hook.calls.first().args[1]).toEqual({a: 42});
- delete Chart.defaults.plugins.a;
+ Chart.unregister(plugin);
});
it('should call plugins with default options if plugin config options is undefined', function() {
- var plugin = {id: 'a', hook: function() {}};
+ var plugin = {id: 'a', hook: function() {}, defaults: {a: 'foobar'}};
- Chart.defaults.plugins.a = {a: 'foobar'};
- Chart.plugins.register(plugin);
+ Chart.register(plugin);
spyOn(plugin, 'hook');
var chart = window.acquireChart();
- Chart.plugins.notify(chart, 'hook');
+ chart._plugins.notify(chart, 'hook');
expect(plugin.hook).toHaveBeenCalled();
expect(plugin.hook.calls.first().args[1]).toEqual({a: 'foobar'});
- delete Chart.defaults.plugins.a;
+ Chart.unregister(plugin);
});
// https://github.com/chartjs/Chart.js/issues/5111#issuecomment-355934167
- it('should invalidate cache when update plugin options', function() {
+ it('should update plugin options', function() {
var plugin = {id: 'a', hook: function() {}};
var chart = window.acquireChart({
plugins: [plugin],
spyOn(plugin, 'hook');
- Chart.plugins.notify(chart, 'hook');
+ chart._plugins.notify(chart, 'hook');
expect(plugin.hook).toHaveBeenCalled();
expect(plugin.hook.calls.first().args[1]).toEqual({foo: 'foo'});
chart.update();
plugin.hook.calls.reset();
- Chart.plugins.notify(chart, 'hook');
+ chart._plugins.notify(chart, 'hook');
expect(plugin.hook).toHaveBeenCalled();
expect(plugin.hook.calls.first().args[1]).toEqual({bar: 'bar'});
expect(Chart.Element instanceof Object).toBeTruthy();
expect(Chart.Interaction instanceof Object).toBeTruthy();
expect(Chart.layouts instanceof Object).toBeTruthy();
- expect(Chart.plugins instanceof Object).toBeTruthy();
expect(Chart.platforms.BasePlatform instanceof Function).toBeTruthy();
expect(Chart.platforms.BasicPlatform instanceof Function).toBeTruthy();
// Test the rectangle element
-var Title = Chart.plugins.getAll().find(p => p.id === 'title')._element;
+var Title = Chart.registry.getPlugin('title')._element;
describe('Title block tests', function() {
it('Should have the correct default config', function() {
- expect(Chart.defaults.title).toEqual({
+ expect(Chart.defaults.plugins.title).toEqual({
align: 'center',
display: false,
position: 'top',
it('should update correctly', function() {
var chart = {};
- var options = Chart.helpers.clone(Chart.defaults.title);
+ var options = Chart.helpers.clone(Chart.defaults.plugins.title);
options.text = 'My title';
var title = new Title({
it('should update correctly when vertical', function() {
var chart = {};
- var options = Chart.helpers.clone(Chart.defaults.title);
+ var options = Chart.helpers.clone(Chart.defaults.plugins.title);
options.text = 'My title';
options.position = 'left';
it('should have the correct size when there are multiple lines of text', function() {
var chart = {};
- var options = Chart.helpers.clone(Chart.defaults.title);
+ var options = Chart.helpers.clone(Chart.defaults.plugins.title);
options.text = ['line1', 'line2'];
options.position = 'left';
options.display = true;
var chart = {};
var context = window.createMockContext();
- var options = Chart.helpers.clone(Chart.defaults.title);
+ var options = Chart.helpers.clone(Chart.defaults.plugins.title);
options.text = 'My title';
var title = new Title({
var chart = {};
var context = window.createMockContext();
- var options = Chart.helpers.clone(Chart.defaults.title);
+ var options = Chart.helpers.clone(Chart.defaults.plugins.title);
options.text = 'My title';
options.position = 'left';
chart.options.title = {};
chart.update();
expect(chart.titleBlock).not.toBe(undefined);
- expect(chart.titleBlock.options).toEqual(jasmine.objectContaining(Chart.defaults.title));
+ expect(chart.titleBlock.options).toEqual(jasmine.objectContaining(Chart.defaults.plugins.title));
});
});
});
// Test the rectangle element
-const tooltipPlugin = Chart.plugins.getAll().find(p => p.id === 'tooltip');
+const tooltipPlugin = Chart.registry.getPlugin('tooltip');
const Tooltip = tooltipPlugin._element;
describe('Plugin.Tooltip', function() {
value: '20'
};
- var label = Chart.defaults.tooltips.callbacks.label(tooltipItem, data);
+ var label = Chart.defaults.plugins.tooltip.callbacks.label(tooltipItem, data);
expect(label).toBe('20');
data.datasets[0].label = 'My dataset';
- label = Chart.defaults.tooltips.callbacks.label(tooltipItem, data);
+ label = Chart.defaults.plugins.tooltip.callbacks.label(tooltipItem, data);
expect(label).toBe('My dataset: 20');
});
});