From: Dan Onoshko Date: Thu, 18 Aug 2022 06:03:12 +0000 (+0400) Subject: feat: tooltip callbacks fallback (#10567) X-Git-Tag: v4.0.0~60 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=ffce0f9f18751bc6f6d6ba17eadf7994b1302a6a;p=thirdparty%2FChart.js.git feat: tooltip callbacks fallback (#10567) * feat: tooltip callbacks fallback * docs: review fixes --- diff --git a/.size-limit.cjs b/.size-limit.cjs index e9ea99edd..b2026cd49 100644 --- a/.size-limit.cjs +++ b/.size-limit.cjs @@ -34,7 +34,7 @@ module.exports = [ }, { path: 'dist/chart.js', - limit: '27 KB', + limit: '27.1 KB', import: '{ Decimation, Filler, Legend, SubTitle, Title, Tooltip }', running: false, modifyWebpackConfig diff --git a/docs/configuration/tooltip.md b/docs/configuration/tooltip.md index a89de4901..db55ca7be 100644 --- a/docs/configuration/tooltip.md +++ b/docs/configuration/tooltip.md @@ -97,7 +97,7 @@ Allows filtering of [tooltip items](#tooltip-item-context). Must implement at mi ## Tooltip Callbacks -Namespace: `options.plugins.tooltip.callbacks`, the tooltip has the following callbacks for providing text. For all functions, `this` will be the tooltip object created from the `Tooltip` constructor. +Namespace: `options.plugins.tooltip.callbacks`, the tooltip has the following callbacks for providing text. For all functions, `this` will be the tooltip object created from the `Tooltip` constructor. If the callback returns `undefined`, then the default callback will be used. To remove things from the tooltip callback should return an empty string. Namespace: `data.datasets[].tooltip.callbacks`, items marked with `Yes` in the column `Dataset override` can be overridden per dataset. @@ -105,20 +105,20 @@ A [tooltip item context](#tooltip-item-context) is generated for each item that | Name | Arguments | Return Type | Dataset override | Description | ---- | --------- | ----------- | ---------------- | ----------- -| `beforeTitle` | `TooltipItem[]` | `string | string[]` | | Returns the text to render before the title. -| `title` | `TooltipItem[]` | `string | string[]` | | Returns text to render as the title of the tooltip. -| `afterTitle` | `TooltipItem[]` | `string | string[]` | | Returns text to render after the title. -| `beforeBody` | `TooltipItem[]` | `string | string[]` | | Returns text to render before the body section. -| `beforeLabel` | `TooltipItem` | `string | string[]` | Yes | Returns text to render before an individual label. This will be called for each item in the tooltip. -| `label` | `TooltipItem` | `string | string[]` | Yes | Returns text to render for an individual item in the tooltip. [more...](#label-callback) -| `labelColor` | `TooltipItem` | `object` | Yes | Returns the colors to render for the tooltip item. [more...](#label-color-callback) -| `labelTextColor` | `TooltipItem` | `Color` | Yes | Returns the colors for the text of the label for the tooltip item. -| `labelPointStyle` | `TooltipItem` | `object` | Yes | Returns the point style to use instead of color boxes if usePointStyle is true (object with values `pointStyle` and `rotation`). Default implementation uses the point style from the dataset points. [more...](#label-point-style-callback) -| `afterLabel` | `TooltipItem` | `string | string[]` | Yes | Returns text to render after an individual label. -| `afterBody` | `TooltipItem[]` | `string | string[]` | | Returns text to render after the body section. -| `beforeFooter` | `TooltipItem[]` | `string | string[]` | | Returns text to render before the footer section. -| `footer` | `TooltipItem[]` | `string | string[]` | | Returns text to render as the footer of the tooltip. -| `afterFooter` | `TooltipItem[]` | `string | string[]` | | Text to render after the footer section. +| `beforeTitle` | `TooltipItem[]` | `string | string[] | undefined` | | Returns the text to render before the title. +| `title` | `TooltipItem[]` | `string | string[] | undefined` | | Returns text to render as the title of the tooltip. +| `afterTitle` | `TooltipItem[]` | `string | string[] | undefined` | | Returns text to render after the title. +| `beforeBody` | `TooltipItem[]` | `string | string[] | undefined` | | Returns text to render before the body section. +| `beforeLabel` | `TooltipItem` | `string | string[] | undefined` | Yes | Returns text to render before an individual label. This will be called for each item in the tooltip. +| `label` | `TooltipItem` | `string | string[] | undefined` | Yes | Returns text to render for an individual item in the tooltip. [more...](#label-callback) +| `labelColor` | `TooltipItem` | `object | undefined` | Yes | Returns the colors to render for the tooltip item. [more...](#label-color-callback) +| `labelTextColor` | `TooltipItem` | `Color | undefined` | Yes | Returns the colors for the text of the label for the tooltip item. +| `labelPointStyle` | `TooltipItem` | `object | undefined` | Yes | Returns the point style to use instead of color boxes if usePointStyle is true (object with values `pointStyle` and `rotation`). Default implementation uses the point style from the dataset points. [more...](#label-point-style-callback) +| `afterLabel` | `TooltipItem` | `string | string[] | undefined` | Yes | Returns text to render after an individual label. +| `afterBody` | `TooltipItem[]` | `string | string[] | undefined` | | Returns text to render after the body section. +| `beforeFooter` | `TooltipItem[]` | `string | string[] | undefined` | | Returns text to render before the footer section. +| `footer` | `TooltipItem[]` | `string | string[] | undefined` | | Returns text to render as the footer of the tooltip. +| `afterFooter` | `TooltipItem[]` | `string | string[] | undefined` | | Text to render after the footer section. ### Label Callback @@ -441,4 +441,4 @@ declare module 'chart.js' { myCustomPositioner: TooltipPositionerFunction; } } -``` \ No newline at end of file +``` diff --git a/docs/migration/v4-migration.md b/docs/migration/v4-migration.md index 8d3c343b1..8a79afe21 100644 --- a/docs/migration/v4-migration.md +++ b/docs/migration/v4-migration.md @@ -13,6 +13,7 @@ A number of changes were made to the configuration options passed to the `Chart` * The radialLinear grid indexable and scriptable options don't decrease the index of the specified grid line anymore. * The `destroy` plugin hook has been removed and replaced with `afterDestroy`. * Ticks callback on time scale now receives timestamp instead of a formatted label. +* If the tooltip callback returns `undefined`, then the default callback will be used. #### Type changes * The order of the `ChartMeta` parameters have been changed from `` to `` diff --git a/src/plugins/plugin.tooltip.js b/src/plugins/plugin.tooltip.js index 77d40be55..bb408a917 100644 --- a/src/plugins/plugin.tooltip.js +++ b/src/plugins/plugin.tooltip.js @@ -350,6 +350,102 @@ function overrideCallbacks(callbacks, context) { return override ? callbacks.override(override) : callbacks; } +const defaultCallbacks = { + // Args are: (tooltipItems, data) + beforeTitle: noop, + title(tooltipItems) { + if (tooltipItems.length > 0) { + const item = tooltipItems[0]; + const labels = item.chart.data.labels; + const labelCount = labels ? labels.length : 0; + + if (this && this.options && this.options.mode === 'dataset') { + return item.dataset.label || ''; + } else if (item.label) { + return item.label; + } else if (labelCount > 0 && item.dataIndex < labelCount) { + return labels[item.dataIndex]; + } + } + + return ''; + }, + afterTitle: noop, + + // Args are: (tooltipItems, data) + beforeBody: noop, + + // Args are: (tooltipItem, data) + beforeLabel: noop, + label(tooltipItem) { + if (this && this.options && this.options.mode === 'dataset') { + return tooltipItem.label + ': ' + tooltipItem.formattedValue || tooltipItem.formattedValue; + } + + let label = tooltipItem.dataset.label || ''; + + if (label) { + label += ': '; + } + const value = tooltipItem.formattedValue; + if (!isNullOrUndef(value)) { + label += value; + } + return label; + }, + labelColor(tooltipItem) { + const meta = tooltipItem.chart.getDatasetMeta(tooltipItem.datasetIndex); + const options = meta.controller.getStyle(tooltipItem.dataIndex); + return { + borderColor: options.borderColor, + backgroundColor: options.backgroundColor, + borderWidth: options.borderWidth, + borderDash: options.borderDash, + borderDashOffset: options.borderDashOffset, + borderRadius: 0, + }; + }, + labelTextColor() { + return this.options.bodyColor; + }, + labelPointStyle(tooltipItem) { + const meta = tooltipItem.chart.getDatasetMeta(tooltipItem.datasetIndex); + const options = meta.controller.getStyle(tooltipItem.dataIndex); + return { + pointStyle: options.pointStyle, + rotation: options.rotation, + }; + }, + afterLabel: noop, + + // Args are: (tooltipItems, data) + afterBody: noop, + + // Args are: (tooltipItems, data) + beforeFooter: noop, + footer: noop, + afterFooter: noop +}; + +/** + * Invoke callback from object with context and arguments. + * If callback returns `undefined`, then will be invoked default callback. + * @param {Record} callbacks + * @param {keyof typeof defaultCallbacks} name + * @param {*} ctx + * @param {*} arg + * @returns {any} + */ +function invokeCallbackWithFallback(callbacks, name, ctx, arg) { + const result = callbacks[name].call(ctx, arg); + + if (typeof result === 'undefined') { + return defaultCallbacks[name].call(ctx, arg); + } + + return result; +} + export class Tooltip extends Element { /** @@ -431,9 +527,9 @@ export class Tooltip extends Element { getTitle(context, options) { const {callbacks} = options; - const beforeTitle = callbacks.beforeTitle.apply(this, [context]); - const title = callbacks.title.apply(this, [context]); - const afterTitle = callbacks.afterTitle.apply(this, [context]); + const beforeTitle = invokeCallbackWithFallback(callbacks, 'beforeTitle', this, context); + const title = invokeCallbackWithFallback(callbacks, 'title', this, context); + const afterTitle = invokeCallbackWithFallback(callbacks, 'afterTitle', this, context); let lines = []; lines = pushOrConcat(lines, splitNewlines(beforeTitle)); @@ -444,7 +540,9 @@ export class Tooltip extends Element { } getBeforeBody(tooltipItems, options) { - return getBeforeAfterBodyLines(options.callbacks.beforeBody.apply(this, [tooltipItems])); + return getBeforeAfterBodyLines( + invokeCallbackWithFallback(options.callbacks, 'beforeBody', this, tooltipItems) + ); } getBody(tooltipItems, options) { @@ -458,9 +556,9 @@ export class Tooltip extends Element { after: [] }; const scoped = overrideCallbacks(callbacks, context); - pushOrConcat(bodyItem.before, splitNewlines(scoped.beforeLabel.call(this, context))); - pushOrConcat(bodyItem.lines, scoped.label.call(this, context)); - pushOrConcat(bodyItem.after, splitNewlines(scoped.afterLabel.call(this, context))); + pushOrConcat(bodyItem.before, splitNewlines(invokeCallbackWithFallback(scoped, 'beforeLabel', this, context))); + pushOrConcat(bodyItem.lines, invokeCallbackWithFallback(scoped, 'label', this, context)); + pushOrConcat(bodyItem.after, splitNewlines(invokeCallbackWithFallback(scoped, 'afterLabel', this, context))); bodyItems.push(bodyItem); }); @@ -469,16 +567,18 @@ export class Tooltip extends Element { } getAfterBody(tooltipItems, options) { - return getBeforeAfterBodyLines(options.callbacks.afterBody.apply(this, [tooltipItems])); + return getBeforeAfterBodyLines( + invokeCallbackWithFallback(options.callbacks, 'afterBody', this, tooltipItems) + ); } // Get the footer and beforeFooter and afterFooter lines getFooter(tooltipItems, options) { const {callbacks} = options; - const beforeFooter = callbacks.beforeFooter.apply(this, [tooltipItems]); - const footer = callbacks.footer.apply(this, [tooltipItems]); - const afterFooter = callbacks.afterFooter.apply(this, [tooltipItems]); + const beforeFooter = invokeCallbackWithFallback(callbacks, 'beforeFooter', this, tooltipItems); + const footer = invokeCallbackWithFallback(callbacks, 'footer', this, tooltipItems); + const afterFooter = invokeCallbackWithFallback(callbacks, 'afterFooter', this, tooltipItems); let lines = []; lines = pushOrConcat(lines, splitNewlines(beforeFooter)); @@ -517,9 +617,9 @@ export class Tooltip extends Element { // Determine colors for boxes each(tooltipItems, (context) => { const scoped = overrideCallbacks(options.callbacks, context); - labelColors.push(scoped.labelColor.call(this, context)); - labelPointStyles.push(scoped.labelPointStyle.call(this, context)); - labelTextColors.push(scoped.labelTextColor.call(this, context)); + labelColors.push(invokeCallbackWithFallback(scoped, 'labelColor', this, context)); + labelPointStyles.push(invokeCallbackWithFallback(scoped, 'labelPointStyle', this, context)); + labelTextColors.push(invokeCallbackWithFallback(scoped, 'labelTextColor', this, context)); }); this.labelColors = labelColors; @@ -1211,82 +1311,7 @@ export default { duration: 200 } }, - callbacks: { - // Args are: (tooltipItems, data) - beforeTitle: noop, - title(tooltipItems) { - if (tooltipItems.length > 0) { - const item = tooltipItems[0]; - const labels = item.chart.data.labels; - const labelCount = labels ? labels.length : 0; - - if (this && this.options && this.options.mode === 'dataset') { - return item.dataset.label || ''; - } else if (item.label) { - return item.label; - } else if (labelCount > 0 && item.dataIndex < labelCount) { - return labels[item.dataIndex]; - } - } - - return ''; - }, - afterTitle: noop, - - // Args are: (tooltipItems, data) - beforeBody: noop, - - // Args are: (tooltipItem, data) - beforeLabel: noop, - label(tooltipItem) { - if (this && this.options && this.options.mode === 'dataset') { - return tooltipItem.label + ': ' + tooltipItem.formattedValue || tooltipItem.formattedValue; - } - - let label = tooltipItem.dataset.label || ''; - - if (label) { - label += ': '; - } - const value = tooltipItem.formattedValue; - if (!isNullOrUndef(value)) { - label += value; - } - return label; - }, - labelColor(tooltipItem) { - const meta = tooltipItem.chart.getDatasetMeta(tooltipItem.datasetIndex); - const options = meta.controller.getStyle(tooltipItem.dataIndex); - return { - borderColor: options.borderColor, - backgroundColor: options.backgroundColor, - borderWidth: options.borderWidth, - borderDash: options.borderDash, - borderDashOffset: options.borderDashOffset, - borderRadius: 0, - }; - }, - labelTextColor() { - return this.options.bodyColor; - }, - labelPointStyle(tooltipItem) { - const meta = tooltipItem.chart.getDatasetMeta(tooltipItem.datasetIndex); - const options = meta.controller.getStyle(tooltipItem.dataIndex); - return { - pointStyle: options.pointStyle, - rotation: options.rotation, - }; - }, - afterLabel: noop, - - // Args are: (tooltipItems, data) - afterBody: noop, - - // Args are: (tooltipItems, data) - beforeFooter: noop, - footer: noop, - afterFooter: noop - } + callbacks: defaultCallbacks }, defaultRoutes: { diff --git a/test/specs/plugin.tooltip.tests.js b/test/specs/plugin.tooltip.tests.js index 8644b34c7..da0563f62 100644 --- a/test/specs/plugin.tooltip.tests.js +++ b/test/specs/plugin.tooltip.tests.js @@ -1698,4 +1698,104 @@ describe('Plugin.Tooltip', function() { expect(chart.tooltip.opacity).toEqual(1); }); }); + + it('should use default callback if user callback returns undefined', async() => { + const chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + plugins: { + tooltip: { + callbacks: { + beforeTitle() { + return undefined; + }, + title() { + return undefined; + }, + afterTitle() { + return undefined; + }, + beforeBody() { + return undefined; + }, + beforeLabel() { + return undefined; + }, + label() { + return undefined; + }, + afterLabel() { + return undefined; + }, + afterBody() { + return undefined; + }, + beforeFooter() { + return undefined; + }, + footer() { + return undefined; + }, + afterFooter() { + return undefined; + }, + labelTextColor() { + return undefined; + }, + labelPointStyle() { + return undefined; + } + } + } + } + } + }); + const {defaults} = Chart; + const {tooltip} = chart; + const point = chart.getDatasetMeta(0).data[0]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + + expect(tooltip).toEqual(jasmine.objectContaining({ + opacity: 1, + + // Text + title: ['Point 1'], + beforeBody: [], + body: [{ + before: [], + lines: ['Dataset 1: 10'], + after: [] + }], + afterBody: [], + footer: [], + labelTextColors: ['#fff'], + labelColors: [{ + borderColor: defaults.borderColor, + backgroundColor: defaults.backgroundColor, + borderWidth: 1, + borderDash: undefined, + borderDashOffset: undefined, + borderRadius: 0, + }], + labelPointStyles: [{ + pointStyle: 'circle', + rotation: 0 + }] + })); + }); }); diff --git a/types/index.d.ts b/types/index.d.ts index 5d1166113..438a7259e 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -2546,24 +2546,24 @@ export interface TooltipCallbacks< Model = TooltipModel, Item = TooltipItem> { - beforeTitle(this: Model, tooltipItems: Item[]): string | string[]; - title(this: Model, tooltipItems: Item[]): string | string[]; - afterTitle(this: Model, tooltipItems: Item[]): string | string[]; + beforeTitle(this: Model, tooltipItems: Item[]): string | string[] | void; + title(this: Model, tooltipItems: Item[]): string | string[] | void; + afterTitle(this: Model, tooltipItems: Item[]): string | string[] | void; - beforeBody(this: Model, tooltipItems: Item[]): string | string[]; - afterBody(this: Model, tooltipItems: Item[]): string | string[]; + beforeBody(this: Model, tooltipItems: Item[]): string | string[] | void; + afterBody(this: Model, tooltipItems: Item[]): string | string[] | void; - beforeLabel(this: Model, tooltipItem: Item): string | string[]; - label(this: Model, tooltipItem: Item): string | string[]; - afterLabel(this: Model, tooltipItem: Item): string | string[]; + beforeLabel(this: Model, tooltipItem: Item): string | string[] | void; + label(this: Model, tooltipItem: Item): string | string[] | void; + afterLabel(this: Model, tooltipItem: Item): string | string[] | void; - labelColor(this: Model, tooltipItem: Item): TooltipLabelStyle; - labelTextColor(this: Model, tooltipItem: Item): Color; - labelPointStyle(this: Model, tooltipItem: Item): { pointStyle: PointStyle; rotation: number }; + labelColor(this: Model, tooltipItem: Item): TooltipLabelStyle | void; + labelTextColor(this: Model, tooltipItem: Item): Color | void; + labelPointStyle(this: Model, tooltipItem: Item): { pointStyle: PointStyle; rotation: number } | void; - beforeFooter(this: Model, tooltipItems: Item[]): string | string[]; - footer(this: Model, tooltipItems: Item[]): string | string[]; - afterFooter(this: Model, tooltipItems: Item[]): string | string[]; + beforeFooter(this: Model, tooltipItems: Item[]): string | string[] | void; + footer(this: Model, tooltipItems: Item[]): string | string[] | void; + afterFooter(this: Model, tooltipItems: Item[]): string | string[] | void; } export interface ExtendedPlugin<