Skip to content

Commit ffce0f9

Browse files
authored
feat: tooltip callbacks fallback (chartjs#10567)
* feat: tooltip callbacks fallback * docs: review fixes
1 parent 7776d27 commit ffce0f9

File tree

6 files changed

+247
-121
lines changed

6 files changed

+247
-121
lines changed

.size-limit.cjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ module.exports = [
3434
},
3535
{
3636
path: 'dist/chart.js',
37-
limit: '27 KB',
37+
limit: '27.1 KB',
3838
import: '{ Decimation, Filler, Legend, SubTitle, Title, Tooltip }',
3939
running: false,
4040
modifyWebpackConfig

docs/configuration/tooltip.md

+16-16
Original file line numberDiff line numberDiff line change
@@ -97,28 +97,28 @@ Allows filtering of [tooltip items](#tooltip-item-context). Must implement at mi
9797

9898
## Tooltip Callbacks
9999

100-
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.
100+
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.
101101

102102
Namespace: `data.datasets[].tooltip.callbacks`, items marked with `Yes` in the column `Dataset override` can be overridden per dataset.
103103

104104
A [tooltip item context](#tooltip-item-context) is generated for each item that appears in the tooltip. This is the primary model that the callback methods interact with. For functions that return text, arrays of strings are treated as multiple lines of text.
105105

106106
| Name | Arguments | Return Type | Dataset override | Description
107107
| ---- | --------- | ----------- | ---------------- | -----------
108-
| `beforeTitle` | `TooltipItem[]` | `string | string[]` | | Returns the text to render before the title.
109-
| `title` | `TooltipItem[]` | `string | string[]` | | Returns text to render as the title of the tooltip.
110-
| `afterTitle` | `TooltipItem[]` | `string | string[]` | | Returns text to render after the title.
111-
| `beforeBody` | `TooltipItem[]` | `string | string[]` | | Returns text to render before the body section.
112-
| `beforeLabel` | `TooltipItem` | `string | string[]` | Yes | Returns text to render before an individual label. This will be called for each item in the tooltip.
113-
| `label` | `TooltipItem` | `string | string[]` | Yes | Returns text to render for an individual item in the tooltip. [more...](#label-callback)
114-
| `labelColor` | `TooltipItem` | `object` | Yes | Returns the colors to render for the tooltip item. [more...](#label-color-callback)
115-
| `labelTextColor` | `TooltipItem` | `Color` | Yes | Returns the colors for the text of the label for the tooltip item.
116-
| `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)
117-
| `afterLabel` | `TooltipItem` | `string | string[]` | Yes | Returns text to render after an individual label.
118-
| `afterBody` | `TooltipItem[]` | `string | string[]` | | Returns text to render after the body section.
119-
| `beforeFooter` | `TooltipItem[]` | `string | string[]` | | Returns text to render before the footer section.
120-
| `footer` | `TooltipItem[]` | `string | string[]` | | Returns text to render as the footer of the tooltip.
121-
| `afterFooter` | `TooltipItem[]` | `string | string[]` | | Text to render after the footer section.
108+
| `beforeTitle` | `TooltipItem[]` | `string | string[] | undefined` | | Returns the text to render before the title.
109+
| `title` | `TooltipItem[]` | `string | string[] | undefined` | | Returns text to render as the title of the tooltip.
110+
| `afterTitle` | `TooltipItem[]` | `string | string[] | undefined` | | Returns text to render after the title.
111+
| `beforeBody` | `TooltipItem[]` | `string | string[] | undefined` | | Returns text to render before the body section.
112+
| `beforeLabel` | `TooltipItem` | `string | string[] | undefined` | Yes | Returns text to render before an individual label. This will be called for each item in the tooltip.
113+
| `label` | `TooltipItem` | `string | string[] | undefined` | Yes | Returns text to render for an individual item in the tooltip. [more...](#label-callback)
114+
| `labelColor` | `TooltipItem` | `object | undefined` | Yes | Returns the colors to render for the tooltip item. [more...](#label-color-callback)
115+
| `labelTextColor` | `TooltipItem` | `Color | undefined` | Yes | Returns the colors for the text of the label for the tooltip item.
116+
| `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)
117+
| `afterLabel` | `TooltipItem` | `string | string[] | undefined` | Yes | Returns text to render after an individual label.
118+
| `afterBody` | `TooltipItem[]` | `string | string[] | undefined` | | Returns text to render after the body section.
119+
| `beforeFooter` | `TooltipItem[]` | `string | string[] | undefined` | | Returns text to render before the footer section.
120+
| `footer` | `TooltipItem[]` | `string | string[] | undefined` | | Returns text to render as the footer of the tooltip.
121+
| `afterFooter` | `TooltipItem[]` | `string | string[] | undefined` | | Text to render after the footer section.
122122

123123
### Label Callback
124124

@@ -441,4 +441,4 @@ declare module 'chart.js' {
441441
myCustomPositioner: TooltipPositionerFunction<ChartType>;
442442
}
443443
}
444-
```
444+
```

docs/migration/v4-migration.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ A number of changes were made to the configuration options passed to the `Chart`
1313
* The radialLinear grid indexable and scriptable options don't decrease the index of the specified grid line anymore.
1414
* The `destroy` plugin hook has been removed and replaced with `afterDestroy`.
1515
* Ticks callback on time scale now receives timestamp instead of a formatted label.
16+
* If the tooltip callback returns `undefined`, then the default callback will be used.
1617

1718
#### Type changes
1819
* The order of the `ChartMeta` parameters have been changed from `<Element, DatasetElement, Type>` to `<Type, Element, DatasetElement>`

src/plugins/plugin.tooltip.js

+115-90
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,102 @@ function overrideCallbacks(callbacks, context) {
350350
return override ? callbacks.override(override) : callbacks;
351351
}
352352

353+
const defaultCallbacks = {
354+
// Args are: (tooltipItems, data)
355+
beforeTitle: noop,
356+
title(tooltipItems) {
357+
if (tooltipItems.length > 0) {
358+
const item = tooltipItems[0];
359+
const labels = item.chart.data.labels;
360+
const labelCount = labels ? labels.length : 0;
361+
362+
if (this && this.options && this.options.mode === 'dataset') {
363+
return item.dataset.label || '';
364+
} else if (item.label) {
365+
return item.label;
366+
} else if (labelCount > 0 && item.dataIndex < labelCount) {
367+
return labels[item.dataIndex];
368+
}
369+
}
370+
371+
return '';
372+
},
373+
afterTitle: noop,
374+
375+
// Args are: (tooltipItems, data)
376+
beforeBody: noop,
377+
378+
// Args are: (tooltipItem, data)
379+
beforeLabel: noop,
380+
label(tooltipItem) {
381+
if (this && this.options && this.options.mode === 'dataset') {
382+
return tooltipItem.label + ': ' + tooltipItem.formattedValue || tooltipItem.formattedValue;
383+
}
384+
385+
let label = tooltipItem.dataset.label || '';
386+
387+
if (label) {
388+
label += ': ';
389+
}
390+
const value = tooltipItem.formattedValue;
391+
if (!isNullOrUndef(value)) {
392+
label += value;
393+
}
394+
return label;
395+
},
396+
labelColor(tooltipItem) {
397+
const meta = tooltipItem.chart.getDatasetMeta(tooltipItem.datasetIndex);
398+
const options = meta.controller.getStyle(tooltipItem.dataIndex);
399+
return {
400+
borderColor: options.borderColor,
401+
backgroundColor: options.backgroundColor,
402+
borderWidth: options.borderWidth,
403+
borderDash: options.borderDash,
404+
borderDashOffset: options.borderDashOffset,
405+
borderRadius: 0,
406+
};
407+
},
408+
labelTextColor() {
409+
return this.options.bodyColor;
410+
},
411+
labelPointStyle(tooltipItem) {
412+
const meta = tooltipItem.chart.getDatasetMeta(tooltipItem.datasetIndex);
413+
const options = meta.controller.getStyle(tooltipItem.dataIndex);
414+
return {
415+
pointStyle: options.pointStyle,
416+
rotation: options.rotation,
417+
};
418+
},
419+
afterLabel: noop,
420+
421+
// Args are: (tooltipItems, data)
422+
afterBody: noop,
423+
424+
// Args are: (tooltipItems, data)
425+
beforeFooter: noop,
426+
footer: noop,
427+
afterFooter: noop
428+
};
429+
430+
/**
431+
* Invoke callback from object with context and arguments.
432+
* If callback returns `undefined`, then will be invoked default callback.
433+
* @param {Record<keyof typeof defaultCallbacks, Function>} callbacks
434+
* @param {keyof typeof defaultCallbacks} name
435+
* @param {*} ctx
436+
* @param {*} arg
437+
* @returns {any}
438+
*/
439+
function invokeCallbackWithFallback(callbacks, name, ctx, arg) {
440+
const result = callbacks[name].call(ctx, arg);
441+
442+
if (typeof result === 'undefined') {
443+
return defaultCallbacks[name].call(ctx, arg);
444+
}
445+
446+
return result;
447+
}
448+
353449
export class Tooltip extends Element {
354450

355451
/**
@@ -431,9 +527,9 @@ export class Tooltip extends Element {
431527
getTitle(context, options) {
432528
const {callbacks} = options;
433529

434-
const beforeTitle = callbacks.beforeTitle.apply(this, [context]);
435-
const title = callbacks.title.apply(this, [context]);
436-
const afterTitle = callbacks.afterTitle.apply(this, [context]);
530+
const beforeTitle = invokeCallbackWithFallback(callbacks, 'beforeTitle', this, context);
531+
const title = invokeCallbackWithFallback(callbacks, 'title', this, context);
532+
const afterTitle = invokeCallbackWithFallback(callbacks, 'afterTitle', this, context);
437533

438534
let lines = [];
439535
lines = pushOrConcat(lines, splitNewlines(beforeTitle));
@@ -444,7 +540,9 @@ export class Tooltip extends Element {
444540
}
445541

446542
getBeforeBody(tooltipItems, options) {
447-
return getBeforeAfterBodyLines(options.callbacks.beforeBody.apply(this, [tooltipItems]));
543+
return getBeforeAfterBodyLines(
544+
invokeCallbackWithFallback(options.callbacks, 'beforeBody', this, tooltipItems)
545+
);
448546
}
449547

450548
getBody(tooltipItems, options) {
@@ -458,9 +556,9 @@ export class Tooltip extends Element {
458556
after: []
459557
};
460558
const scoped = overrideCallbacks(callbacks, context);
461-
pushOrConcat(bodyItem.before, splitNewlines(scoped.beforeLabel.call(this, context)));
462-
pushOrConcat(bodyItem.lines, scoped.label.call(this, context));
463-
pushOrConcat(bodyItem.after, splitNewlines(scoped.afterLabel.call(this, context)));
559+
pushOrConcat(bodyItem.before, splitNewlines(invokeCallbackWithFallback(scoped, 'beforeLabel', this, context)));
560+
pushOrConcat(bodyItem.lines, invokeCallbackWithFallback(scoped, 'label', this, context));
561+
pushOrConcat(bodyItem.after, splitNewlines(invokeCallbackWithFallback(scoped, 'afterLabel', this, context)));
464562

465563
bodyItems.push(bodyItem);
466564
});
@@ -469,16 +567,18 @@ export class Tooltip extends Element {
469567
}
470568

471569
getAfterBody(tooltipItems, options) {
472-
return getBeforeAfterBodyLines(options.callbacks.afterBody.apply(this, [tooltipItems]));
570+
return getBeforeAfterBodyLines(
571+
invokeCallbackWithFallback(options.callbacks, 'afterBody', this, tooltipItems)
572+
);
473573
}
474574

475575
// Get the footer and beforeFooter and afterFooter lines
476576
getFooter(tooltipItems, options) {
477577
const {callbacks} = options;
478578

479-
const beforeFooter = callbacks.beforeFooter.apply(this, [tooltipItems]);
480-
const footer = callbacks.footer.apply(this, [tooltipItems]);
481-
const afterFooter = callbacks.afterFooter.apply(this, [tooltipItems]);
579+
const beforeFooter = invokeCallbackWithFallback(callbacks, 'beforeFooter', this, tooltipItems);
580+
const footer = invokeCallbackWithFallback(callbacks, 'footer', this, tooltipItems);
581+
const afterFooter = invokeCallbackWithFallback(callbacks, 'afterFooter', this, tooltipItems);
482582

483583
let lines = [];
484584
lines = pushOrConcat(lines, splitNewlines(beforeFooter));
@@ -517,9 +617,9 @@ export class Tooltip extends Element {
517617
// Determine colors for boxes
518618
each(tooltipItems, (context) => {
519619
const scoped = overrideCallbacks(options.callbacks, context);
520-
labelColors.push(scoped.labelColor.call(this, context));
521-
labelPointStyles.push(scoped.labelPointStyle.call(this, context));
522-
labelTextColors.push(scoped.labelTextColor.call(this, context));
620+
labelColors.push(invokeCallbackWithFallback(scoped, 'labelColor', this, context));
621+
labelPointStyles.push(invokeCallbackWithFallback(scoped, 'labelPointStyle', this, context));
622+
labelTextColors.push(invokeCallbackWithFallback(scoped, 'labelTextColor', this, context));
523623
});
524624

525625
this.labelColors = labelColors;
@@ -1211,82 +1311,7 @@ export default {
12111311
duration: 200
12121312
}
12131313
},
1214-
callbacks: {
1215-
// Args are: (tooltipItems, data)
1216-
beforeTitle: noop,
1217-
title(tooltipItems) {
1218-
if (tooltipItems.length > 0) {
1219-
const item = tooltipItems[0];
1220-
const labels = item.chart.data.labels;
1221-
const labelCount = labels ? labels.length : 0;
1222-
1223-
if (this && this.options && this.options.mode === 'dataset') {
1224-
return item.dataset.label || '';
1225-
} else if (item.label) {
1226-
return item.label;
1227-
} else if (labelCount > 0 && item.dataIndex < labelCount) {
1228-
return labels[item.dataIndex];
1229-
}
1230-
}
1231-
1232-
return '';
1233-
},
1234-
afterTitle: noop,
1235-
1236-
// Args are: (tooltipItems, data)
1237-
beforeBody: noop,
1238-
1239-
// Args are: (tooltipItem, data)
1240-
beforeLabel: noop,
1241-
label(tooltipItem) {
1242-
if (this && this.options && this.options.mode === 'dataset') {
1243-
return tooltipItem.label + ': ' + tooltipItem.formattedValue || tooltipItem.formattedValue;
1244-
}
1245-
1246-
let label = tooltipItem.dataset.label || '';
1247-
1248-
if (label) {
1249-
label += ': ';
1250-
}
1251-
const value = tooltipItem.formattedValue;
1252-
if (!isNullOrUndef(value)) {
1253-
label += value;
1254-
}
1255-
return label;
1256-
},
1257-
labelColor(tooltipItem) {
1258-
const meta = tooltipItem.chart.getDatasetMeta(tooltipItem.datasetIndex);
1259-
const options = meta.controller.getStyle(tooltipItem.dataIndex);
1260-
return {
1261-
borderColor: options.borderColor,
1262-
backgroundColor: options.backgroundColor,
1263-
borderWidth: options.borderWidth,
1264-
borderDash: options.borderDash,
1265-
borderDashOffset: options.borderDashOffset,
1266-
borderRadius: 0,
1267-
};
1268-
},
1269-
labelTextColor() {
1270-
return this.options.bodyColor;
1271-
},
1272-
labelPointStyle(tooltipItem) {
1273-
const meta = tooltipItem.chart.getDatasetMeta(tooltipItem.datasetIndex);
1274-
const options = meta.controller.getStyle(tooltipItem.dataIndex);
1275-
return {
1276-
pointStyle: options.pointStyle,
1277-
rotation: options.rotation,
1278-
};
1279-
},
1280-
afterLabel: noop,
1281-
1282-
// Args are: (tooltipItems, data)
1283-
afterBody: noop,
1284-
1285-
// Args are: (tooltipItems, data)
1286-
beforeFooter: noop,
1287-
footer: noop,
1288-
afterFooter: noop
1289-
}
1314+
callbacks: defaultCallbacks
12901315
},
12911316

12921317
defaultRoutes: {

0 commit comments

Comments
 (0)