]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
feat: tooltip callbacks fallback (#10567)
authorDan Onoshko <danon0404@gmail.com>
Thu, 18 Aug 2022 06:03:12 +0000 (10:03 +0400)
committerGitHub <noreply@github.com>
Thu, 18 Aug 2022 06:03:12 +0000 (09:03 +0300)
* feat: tooltip callbacks fallback

* docs: review fixes

.size-limit.cjs
docs/configuration/tooltip.md
docs/migration/v4-migration.md
src/plugins/plugin.tooltip.js
test/specs/plugin.tooltip.tests.js
types/index.d.ts

index e9ea99edd66d8484a4776a0ab2c9b5a7b66bbcbb..b2026cd49be08d71fa5e5ea67b62097d72ceb7a4 100644 (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
index a89de49012555c091c1758b556f4272b61c99841..db55ca7bebbf7b347c955fccadedaa3608bd1354 100644 (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>;
   }
 }
-```
\ No newline at end of file
+```
index 8d3c343b1448274c6835aec9f91c62faf7ee75d9..8a79afe218f302af46ff065ede1eca9946a30b8e 100644 (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>`
index 77d40be551625e55455f6c91b7072254619f2994..bb408a9177f53bbaa8b494418ca23a262baa178f 100644 (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: {
index 8644b34c794178b5df39be81b2ffa8eef4b4f170..da0563f6253418f7e55d597769f71f74102c3b56 100644 (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
+      }]
+    }));
+  });
 });
index 5d11661133a6b237d0f0be3e617e71522dcf15c0..438a7259ec1c842bd5e8c53b8ef42feefb55efbd 100644 (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<