mirror of
https://github.com/chartjs/Chart.js.git
synced 2025-12-08 20:36:08 +00:00
feat: tooltip callbacks fallback (#10567)
* feat: tooltip callbacks fallback * docs: review fixes
This commit is contained in:
parent
7776d27268
commit
ffce0f9f18
@ -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
|
||||
|
||||
@ -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>;
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
@ -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>`
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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
28
types/index.d.ts
vendored
@ -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<
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user