diff --git a/docs/docs/general/options.md b/docs/docs/general/options.md index f7074356651..399c1d43cfa 100644 --- a/docs/docs/general/options.md +++ b/docs/docs/general/options.md @@ -37,14 +37,52 @@ color: [ ## Option Context The option context is used to give contextual information when resolving options and currently only applies to [scriptable options](#scriptable-options). +The object is preserved, so it can be used to store and pass information between calls. + +There are multiple levels of context objects: + +- `chart` + - `dataset` + - `data` + - `scale` + - `tick` + +Each level inherits its parent(s) and any contextual information stored in the parent is available through the child. The context object contains the following properties: +### chart + - `chart`: the associated chart -- `dataPoint`: the parsed data values for the given `dataIndex` and `datasetIndex` -- `dataIndex`: index of the current data + +### dataset + +In addition to [chart](#chart) + +- `active`: true if element is active (hovered) - `dataset`: dataset at index `datasetIndex` - `datasetIndex`: index of the current dataset +- `index`: getter for `datasetIndex` + +### data + +In addition to [dataset](#dataset) + - `active`: true if element is active (hovered) +- `dataIndex`: index of the current data +- `dataPoint`: the parsed data values for the given `dataIndex` and `datasetIndex` +- `element`: the element (point, arc, bar, etc.) for this data +- `index`: getter for `dataIndex` + +### scale + +In addition to [chart](#chart) + +- `scale`: the associated scale + +### tick + +In addition to [scale](#scale) -**Important**: since the context can represent different types of entities (dataset, data, ticks, etc.), some properties may be `undefined` so be sure to test any context property before using it. +- `tick`: the associated tick object +- `index`: tick index diff --git a/samples/animations/delay.html b/samples/animations/delay.html index 5c9d3fc8a8f..ea38a50d1f8 100644 --- a/samples/animations/delay.html +++ b/samples/animations/delay.html @@ -63,7 +63,6 @@ }; window.onload = function() { var ctx = document.getElementById('canvas').getContext('2d'); - var started = {}; window.myBar = new Chart(ctx, { type: 'bar', data: barChartData, @@ -77,9 +76,9 @@ var delay = 0; var dsIndex = context.datasetIndex; var index = context.dataIndex; - if (!started[index + dsIndex * 1000]) { + if (context.dataPoint && !context.delayed) { delay = index * 300 + dsIndex * 100; - started[index + dsIndex * 1000] = true; + context.delayed = true; } return { easing: 'linear', diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 20e097f3e6e..13d3f8d0fbb 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -116,6 +116,7 @@ class Chart { this._hiddenIndices = {}; this.attached = false; this._animationsDisabled = undefined; + this.$context = undefined; // Add the chart instance to the global namespace Chart.instances[me.id] = me; @@ -712,6 +713,14 @@ class Chart { return meta; } + getContext() { + return this.$context || (this.$context = Object.create(null, { + chart: { + value: this + } + })); + } + getVisibleDatasetCount() { return this.getSortedVisibleDatasetMetas().length; } diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index 6c9dc259c1e..d9c2bd5c742 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -148,6 +148,49 @@ function getFirstScaleId(chart, axis) { return Object.keys(scales).filter(key => scales[key].axis === axis).shift(); } +function createDatasetContext(parent, index, dataset) { + return Object.create(parent, { + active: { + writable: true, + value: false + }, + dataset: { + value: dataset + }, + datasetIndex: { + value: index + }, + index: { + get() { + return this.datasetIndex; + } + } + }); +} + +function createDataContext(parent, index, point, element) { + return Object.create(parent, { + active: { + writable: true, + value: false + }, + dataIndex: { + value: index + }, + dataPoint: { + value: point + }, + element: { + value: element + }, + index: { + get() { + return this.dataIndex; + } + } + }); +} + 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'; @@ -177,6 +220,7 @@ export default class DatasetController { this._drawStart = undefined; this._drawCount = undefined; this.enableOptionSharing = false; + this.$context = undefined; this.initialize(); } @@ -491,6 +535,13 @@ export default class DatasetController { return this._cachedMeta._parsed[index]; } + /** + * @protected + */ + getDataElement(index) { + return this._cachedMeta.data[index]; + } + /** * @protected */ @@ -698,14 +749,18 @@ export default class DatasetController { * @protected */ getContext(index, active) { - return { - chart: this.chart, - dataPoint: this.getParsed(index), - dataIndex: index, - dataset: this.getDataset(), - datasetIndex: this.index, - active - }; + const me = this; + let context; + if (index >= 0 && index < me._cachedMeta.data.length) { + const element = me._cachedMeta.data[index]; + context = element.$context || + (element.$context = createDataContext(me.getContext(), index, me.getParsed(index), element)); + } else { + context = me.$context || (me.$context = createDatasetContext(me.chart.getContext(), me.index, me.getDataset())); + } + + context.active = !!active; + return context; } /** diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 3e04ddfc4e2..84cd4e3d57e 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -8,7 +8,7 @@ import Ticks from './core.ticks'; /** * @typedef { import("./core.controller").default } Chart - * @typedef {{value:any, label?:string, major?:boolean}} Tick + * @typedef {{value:any, label?:string, major?:boolean, $context?:any}} Tick */ defaults.set('scale', { @@ -269,6 +269,25 @@ function skip(ticks, newTicks, spacing, majorStart, majorEnd) { } } +function createScaleContext(parent, scale) { + return Object.create(parent, { + scale: { + value: scale + }, + }); +} + +function createTickContext(parent, index, tick) { + return Object.create(parent, { + tick: { + value: tick + }, + index: { + value: index + } + }); +} + export default class Scale extends Element { // eslint-disable-next-line max-statements @@ -345,6 +364,7 @@ export default class Scale extends Element { this._ticksLength = 0; this._borderValue = 0; this._cache = {}; + this.$context = undefined; } /** @@ -1043,13 +1063,16 @@ export default class Scale extends Element { * @protected */ getContext(index) { - const ticks = this.ticks || []; - return { - chart: this.chart, - scale: this, - tick: ticks[index], - index - }; + const me = this; + const ticks = me.ticks || []; + + if (index >= 0 && index < ticks.length) { + const tick = ticks[index]; + return tick.$context || + (tick.$context = createTickContext(me.getContext(), index, tick)); + } + return me.$context || + (me.$context = createScaleContext(me.chart.getContext(), me)); } /** diff --git a/test/fixtures/controller.line/backgroundColor/scriptable.js b/test/fixtures/controller.line/backgroundColor/scriptable.js index ec4c653fbcc..a1844973a5b 100644 --- a/test/fixtures/controller.line/backgroundColor/scriptable.js +++ b/test/fixtures/controller.line/backgroundColor/scriptable.js @@ -8,7 +8,7 @@ module.exports = { // option in dataset data: [4, 5, 10, null, -10, -5], backgroundColor: function(ctx) { - var index = (ctx.dataIndex === undefined ? ctx.datasetIndex : ctx.dataIndex); + var index = ctx.index; return index === 0 ? '#ff0000' : index === 1 ? '#00ff00' : '#ff00ff'; @@ -26,7 +26,7 @@ module.exports = { elements: { line: { backgroundColor: function(ctx) { - var index = (ctx.dataIndex === undefined ? ctx.datasetIndex : ctx.dataIndex); + var index = ctx.index; return index === 0 ? '#ff0000' : index === 1 ? '#00ff00' : '#ff00ff'; diff --git a/test/fixtures/controller.line/borderColor/scriptable.js b/test/fixtures/controller.line/borderColor/scriptable.js index 15137ea0258..33941b9f547 100644 --- a/test/fixtures/controller.line/borderColor/scriptable.js +++ b/test/fixtures/controller.line/borderColor/scriptable.js @@ -8,7 +8,7 @@ module.exports = { // option in dataset data: [4, 5, 10, null, -10, -5], borderColor: function(ctx) { - var index = (ctx.dataIndex === undefined ? ctx.datasetIndex : ctx.dataIndex); + var index = ctx.index; return index === 0 ? '#ff0000' : index === 1 ? '#00ff00' : '#0000ff'; @@ -26,7 +26,7 @@ module.exports = { elements: { line: { borderColor: function(ctx) { - var index = (ctx.dataIndex === undefined ? ctx.datasetIndex : ctx.dataIndex); + var index = ctx.index; return index === 0 ? '#ff0000' : index === 1 ? '#00ff00' : '#0000ff'; diff --git a/test/fixtures/controller.line/borderWidth/scriptable.js b/test/fixtures/controller.line/borderWidth/scriptable.js index 244c7d9cd65..f4167d7c013 100644 --- a/test/fixtures/controller.line/borderWidth/scriptable.js +++ b/test/fixtures/controller.line/borderWidth/scriptable.js @@ -9,7 +9,7 @@ module.exports = { data: [4, 5, 10, null, -10, -5], borderColor: '#0000ff', borderWidth: function(ctx) { - var index = (ctx.dataIndex === undefined ? ctx.datasetIndex : ctx.dataIndex); + var index = ctx.index; return index % 2 ? 10 : 20; }, pointBorderColor: '#00ff00' @@ -27,7 +27,7 @@ module.exports = { line: { borderColor: '#ff0000', borderWidth: function(ctx) { - var index = (ctx.dataIndex === undefined ? ctx.datasetIndex : ctx.dataIndex); + var index = ctx.index; return index % 2 ? 10 : 20; }, fill: false, diff --git a/test/fixtures/controller.radar/backgroundColor/scriptable.js b/test/fixtures/controller.radar/backgroundColor/scriptable.js index 0f5d313dd3a..2ff729099a3 100644 --- a/test/fixtures/controller.radar/backgroundColor/scriptable.js +++ b/test/fixtures/controller.radar/backgroundColor/scriptable.js @@ -8,7 +8,7 @@ module.exports = { // option in dataset data: [0, 5, 10, null, -10, -5], backgroundColor: function(ctx) { - var index = (ctx.dataIndex === undefined ? ctx.datasetIndex : ctx.dataIndex); + var index = ctx.index; return index === 0 ? '#ff0000' : index === 1 ? '#00ff00' : '#ff00ff'; @@ -26,7 +26,7 @@ module.exports = { elements: { line: { backgroundColor: function(ctx) { - var index = (ctx.dataIndex === undefined ? ctx.datasetIndex : ctx.dataIndex); + var index = ctx.index; return index === 0 ? '#ff0000' : index === 1 ? '#00ff00' : '#ff00ff'; diff --git a/test/fixtures/controller.radar/borderColor/scriptable.js b/test/fixtures/controller.radar/borderColor/scriptable.js index c1cdbb6f846..2767e9786fe 100644 --- a/test/fixtures/controller.radar/borderColor/scriptable.js +++ b/test/fixtures/controller.radar/borderColor/scriptable.js @@ -8,7 +8,7 @@ module.exports = { // option in dataset data: [0, 5, 10, null, -10, -5], borderColor: function(ctx) { - var index = (ctx.dataIndex === undefined ? ctx.datasetIndex : ctx.dataIndex); + var index = ctx.index; return index === 0 ? '#ff0000' : index === 1 ? '#00ff00' : '#0000ff'; @@ -26,7 +26,7 @@ module.exports = { elements: { line: { borderColor: function(ctx) { - var index = (ctx.dataIndex === undefined ? ctx.datasetIndex : ctx.dataIndex); + var index = ctx.index; return index === 0 ? '#ff0000' : index === 1 ? '#00ff00' : '#0000ff'; diff --git a/test/fixtures/controller.radar/borderWidth/scriptable.js b/test/fixtures/controller.radar/borderWidth/scriptable.js index adf8338b8fd..64fcffad988 100644 --- a/test/fixtures/controller.radar/borderWidth/scriptable.js +++ b/test/fixtures/controller.radar/borderWidth/scriptable.js @@ -9,7 +9,7 @@ module.exports = { data: [0, 5, 10, null, -10, -5], borderColor: '#0000ff', borderWidth: function(ctx) { - var index = (ctx.dataIndex === undefined ? ctx.datasetIndex : ctx.dataIndex); + var index = ctx.index; return index % 2 ? 10 : 20; }, pointBorderColor: '#00ff00' @@ -27,7 +27,7 @@ module.exports = { line: { borderColor: '#ff0000', borderWidth: function(ctx) { - var index = (ctx.dataIndex === undefined ? ctx.datasetIndex : ctx.dataIndex); + var index = ctx.index; return index % 2 ? 10 : 20; }, fill: false