feat: tooltip callbacks fallback (#10567)

* feat: tooltip callbacks fallback

* docs: review fixes
This commit is contained in:
Dan Onoshko 2022-08-18 10:03:12 +04:00 committed by GitHub
parent 7776d27268
commit ffce0f9f18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 247 additions and 121 deletions

View File

@ -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

View File

@ -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<ChartType>;
}
}
```
```

View File

@ -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 `<Element, DatasetElement, Type>` to `<Type, Element, DatasetElement>`

View File

@ -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<keyof typeof defaultCallbacks, Function>} 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: {

View File

@ -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
}]
}));
});
});

28
types/index.d.ts vendored
View File

@ -2546,24 +2546,24 @@ export interface TooltipCallbacks<
Model = TooltipModel<TType>,
Item = TooltipItem<TType>> {
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<