]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Improvements to tooltip positioners (#9944)
authorJosh Kelley <joshkel@gmail.com>
Mon, 6 Dec 2021 12:39:06 +0000 (07:39 -0500)
committerGitHub <noreply@github.com>
Mon, 6 Dec 2021 12:39:06 +0000 (07:39 -0500)
* Improve positioner types; allow overriding xAlign and yAlign

* More type improvements; pass in Chart as third parameter

* Expose chart as part of TooltipModel

I initially passed the Chart element as the third parameter to the positioner; however, Scale and LegendElement elements expose `this.chart`, and sample code for positioners used `this._chart`, so documenting the chart member and giving it a public name seems to make more sense.

* Update documentation

* Fix documentation

* Fix issues from code review

docs/configuration/tooltip.md
docs/samples/tooltip/position.md
src/plugins/plugin.tooltip.js
test/specs/plugin.tooltip.tests.js
types/index.esm.d.ts

index 53fd7271635cd1377eac53464b18c0c6c3ad0983..a89de49012555c091c1758b556f4272b61c99841 100644 (file)
@@ -59,31 +59,7 @@ Possible modes are:
 
 `'average'` mode will place the tooltip at the average position of the items displayed in the tooltip. `'nearest'` will place the tooltip at the position of the element closest to the event position.
 
-New modes can be defined by adding functions to the `Chart.Tooltip.positioners` map.
-
-Example:
-
-```javascript
-/**
- * Custom positioner
- * @function Tooltip.positioners.myCustomPositioner
- * @param elements {Chart.Element[]} the tooltip elements
- * @param eventPosition {Point} the position of the event in canvas coordinates
- * @returns {Point} the tooltip position
- */
-const tooltipPlugin = Chart.registry.getPlugin('tooltip');
-tooltipPlugin.positioners.myCustomPositioner = function(elements, eventPosition) {
-    /** @type {Tooltip} */
-    const tooltip = this;
-
-    /* ... */
-
-    return {
-        x: 0,
-        y: 0
-    };
-};
-```
+You can also define [custom position modes](#custom-position-modes).
 
 ### Tooltip Alignment
 
@@ -363,6 +339,8 @@ The tooltip model contains parameters that can be used to render the tooltip.
 
 ```javascript
 {
+    chart: Chart,
+
     // The items that we are rendering in the tooltip. See Tooltip Item Interface section
     dataPoints: TooltipItem[],
 
@@ -407,6 +385,60 @@ The tooltip model contains parameters that can be used to render the tooltip.
     opacity: number,
 
     // tooltip options
-    options : Object
+    options: Object
 }
 ```
+
+## Custom Position Modes
+
+New modes can be defined by adding functions to the `Chart.Tooltip.positioners` map.
+
+Example:
+
+```javascript
+import { Tooltip } from 'chart.js';
+
+/**
+ * Custom positioner
+ * @function Tooltip.positioners.myCustomPositioner
+ * @param elements {Chart.Element[]} the tooltip elements
+ * @param eventPosition {Point} the position of the event in canvas coordinates
+ * @returns {TooltipPosition} the tooltip position
+ */
+Tooltip.positioners.myCustomPositioner = function(elements, eventPosition) {
+    // A reference to the tooltip model
+    const tooltip = this;
+
+    /* ... */
+
+    return {
+        x: 0,
+        y: 0
+        // You may also include xAlign and yAlign to override those tooltip options.
+    };
+};
+
+// Then, to use it...
+new Chart.js(ctx, {
+    data,
+    options: {
+        plugins: {
+            tooltip: {
+                position: 'myCustomPositioner'
+            }
+        }
+    }
+})
+```
+
+See [samples](/samples/tooltip/position.md) for a more detailed example.
+
+If you're using TypeScript, you'll also need to register the new mode:
+
+```typescript
+declare module 'chart.js' {
+  interface TooltipPositionerMap {
+    myCustomPositioner: TooltipPositionerFunction<ChartType>;
+  }
+}
+```
\ No newline at end of file
index fba8eaa3615a6d02987d7f659ad9e20bd54241cc..c2cd854ef8d2f29d6e078a6c311cdf86a2931f94 100644 (file)
@@ -63,11 +63,13 @@ components.Tooltip.positioners.bottom = function(items) {
     return false;
   }
 
-  const chart = this._chart;
+  const chart = this.chart;
 
   return {
     x: pos.x,
     y: chart.chartArea.bottom,
+    xAlign: 'center',
+    yAlign: 'bottom',
   };
 };
 
index 323bafa3a8d2cb70b53181b3b3756c677b2db849..6d91c7ac8f979bb64f196f1ff3b26b3478e4931d 100644 (file)
@@ -8,7 +8,9 @@ import {distanceBetweenPoints, _limitValue} from '../helpers/helpers.math';
 import {createContext, drawPoint} from '../helpers';
 
 /**
+ * @typedef { import("../platform/platform.base").Chart } Chart
  * @typedef { import("../platform/platform.base").ChartEvent } ChartEvent
+ * @typedef { import("../../types/index.esm").ActiveElement } ActiveElement
  */
 
 const positioners = {
@@ -110,7 +112,8 @@ function splitNewlines(str) {
 
 /**
  * Private helper to create a tooltip item model
- * @param item - {element, index, datasetIndex} to create the tooltip item for
+ * @param {Chart} chart
+ * @param {ActiveElement} item - {element, index, datasetIndex} to create the tooltip item for
  * @return new tooltip item
  */
 function createTooltipItem(chart, item) {
@@ -135,7 +138,7 @@ function createTooltipItem(chart, item) {
  * Get the size of the tooltip
  */
 function getTooltipSize(tooltip, options) {
-  const ctx = tooltip._chart.ctx;
+  const ctx = tooltip.chart.ctx;
   const {body, footer, title} = tooltip;
   const {boxWidth, boxHeight} = options;
   const bodyFont = toFont(options.bodyFont);
@@ -256,10 +259,10 @@ function determineXAlign(chart, options, size, yAlign) {
  * Helper to get the alignment of a tooltip given the size
  */
 function determineAlignment(chart, options, size) {
-  const yAlign = options.yAlign || determineYAlign(chart, size);
+  const yAlign = size.yAlign || options.yAlign || determineYAlign(chart, size);
 
   return {
-    xAlign: options.xAlign || determineXAlign(chart, options, size, yAlign),
+    xAlign: size.xAlign || options.xAlign || determineXAlign(chart, options, size, yAlign),
     yAlign
   };
 }
@@ -353,13 +356,15 @@ export class Tooltip extends Element {
 
     this.opacity = 0;
     this._active = [];
-    this._chart = config._chart;
     this._eventPosition = undefined;
     this._size = undefined;
     this._cachedAnimations = undefined;
     this._tooltipItems = [];
     this.$animations = undefined;
     this.$context = undefined;
+    // TODO: V4, remove config._chart and this._chart backward compatibility aliases
+    this.chart = config.chart || config._chart;
+    this._chart = this.chart;
     this.options = config.options;
     this.dataPoints = undefined;
     this.title = undefined;
@@ -398,10 +403,10 @@ export class Tooltip extends Element {
       return cached;
     }
 
-    const chart = this._chart;
+    const chart = this.chart;
     const options = this.options.setContext(this.getContext());
     const opts = options.enabled && chart.options.animation && options.animations;
-    const animations = new Animations(this._chart, opts);
+    const animations = new Animations(this.chart, opts);
     if (opts._cacheable) {
       this._cachedAnimations = Object.freeze(animations);
     }
@@ -414,7 +419,7 @@ export class Tooltip extends Element {
         */
   getContext() {
     return this.$context ||
-                       (this.$context = createTooltipContext(this._chart.getContext(), this, this._tooltipItems));
+                       (this.$context = createTooltipContext(this.chart.getContext(), this, this._tooltipItems));
   }
 
   getTitle(context, options) {
@@ -482,7 +487,7 @@ export class Tooltip extends Element {
         */
   _createItems(options) {
     const active = this._active;
-    const data = this._chart.data;
+    const data = this.chart.data;
     const labelColors = [];
     const labelPointStyles = [];
     const labelTextColors = [];
@@ -490,7 +495,7 @@ export class Tooltip extends Element {
     let i, len;
 
     for (i = 0, len = active.length; i < len; ++i) {
-      tooltipItems.push(createTooltipItem(this._chart, active[i]));
+      tooltipItems.push(createTooltipItem(this.chart, active[i]));
     }
 
     // If the user provided a filter function, use it to modify the tooltip items
@@ -542,8 +547,8 @@ export class Tooltip extends Element {
 
       const size = this._size = getTooltipSize(this, options);
       const positionAndSize = Object.assign({}, position, size);
-      const alignment = determineAlignment(this._chart, options, positionAndSize);
-      const backgroundPoint = getBackgroundPoint(options, positionAndSize, alignment, this._chart);
+      const alignment = determineAlignment(this.chart, options, positionAndSize);
+      const backgroundPoint = getBackgroundPoint(options, positionAndSize, alignment, this.chart);
 
       this.xAlign = alignment.xAlign;
       this.yAlign = alignment.yAlign;
@@ -567,7 +572,7 @@ export class Tooltip extends Element {
     }
 
     if (changed && options.external) {
-      options.external.call(this, {chart: this._chart, tooltip: this, replay});
+      options.external.call(this, {chart: this.chart, tooltip: this, replay});
     }
   }
 
@@ -887,7 +892,7 @@ export class Tooltip extends Element {
         * @private
         */
   _updateAnimationTarget(options) {
-    const chart = this._chart;
+    const chart = this.chart;
     const anims = this.$animations;
     const animX = anims && anims.x;
     const animY = anims && anims.y;
@@ -981,7 +986,7 @@ export class Tooltip extends Element {
   setActiveElements(activeElements, eventPosition) {
     const lastActive = this._active;
     const active = activeElements.map(({datasetIndex, index}) => {
-      const meta = this._chart.getDatasetMeta(datasetIndex);
+      const meta = this.chart.getDatasetMeta(datasetIndex);
 
       if (!meta) {
         throw new Error('Cannot find a dataset at index ' + datasetIndex);
@@ -1017,7 +1022,7 @@ export class Tooltip extends Element {
 
     // Find Active Elements for tooltips
     if (e.type !== 'mouseout') {
-      active = this._chart.getElementsAtEventForMode(e, options.mode, options, replay);
+      active = this.chart.getElementsAtEventForMode(e, options.mode, options, replay);
       if (options.reverse) {
         active.reverse();
       }
@@ -1074,7 +1079,7 @@ export default {
 
   afterInit(chart, _args, options) {
     if (options) {
-      chart.tooltip = new Tooltip({_chart: chart, options});
+      chart.tooltip = new Tooltip({chart, options});
     }
   },
 
index ef0945d9537620a1b683d976e33058efddb2f850..d181c870ca14db3256ba4353a9fabb0cf2e813a2 100644 (file)
@@ -1419,7 +1419,7 @@ describe('Plugin.Tooltip', function() {
 
     var mockContext = window.createMockContext();
     var tooltip = new Tooltip({
-      _chart: {
+      chart: {
         getContext: () => ({}),
         options: {
           plugins: {
index d848b54d17bc1f6c07dbac404ed706e8b6597196..9534d523c4bd5a042380c8d34d07e5e94241e15a 100644 (file)
@@ -2429,7 +2429,9 @@ export interface TooltipLabelStyle {
    */
   borderRadius?: number | BorderRadius;
 }
-export interface TooltipModel<TType extends ChartType> {
+export interface TooltipModel<TType extends ChartType> extends Element<AnyObject, TooltipOptions<TType>> {
+  readonly chart: Chart<TType>;
+
   // The items that we are rendering in the tooltip. See Tooltip Item Interface section
   dataPoints: TooltipItem<TType>[];
 
@@ -2478,14 +2480,34 @@ export interface TooltipModel<TType extends ChartType> {
   options: TooltipOptions<TType>;
 
   getActiveElements(): ActiveElement[];
-  setActiveElements(active: ActiveDataPoint[], eventPosition: { x: number, y: number }): void;
+  setActiveElements(active: ActiveDataPoint[], eventPosition: Point): void;
 }
 
-export const Tooltip: Plugin & {
-  readonly positioners: {
-    [key: string]: (items: readonly ActiveElement[], eventPosition: { x: number; y: number }) => { x: number; y: number } | false;
-  };
-};
+export interface TooltipPosition {
+  x: number;
+  y: number;
+  xAlign?: TooltipXAlignment;
+  yAlign?: TooltipYAlignment;
+}
+
+export type TooltipPositionerFunction<TType extends ChartType> = (
+  this: TooltipModel<TType>,
+  items: readonly ActiveElement[],
+  eventPosition: Point
+) => TooltipPosition | false;
+
+export interface TooltipPositionerMap {
+  average: TooltipPositionerFunction<ChartType>;
+  nearest: TooltipPositionerFunction<ChartType>;
+}
+
+export type TooltipPositioner = keyof TooltipPositionerMap;
+
+export interface Tooltip extends Plugin {
+  readonly positioners: TooltipPositionerMap;
+}
+
+export const Tooltip: Tooltip;
 
 export interface TooltipCallbacks<
   TType extends ChartType,
@@ -2556,7 +2578,7 @@ export interface TooltipOptions<TType extends ChartType = ChartType> extends Cor
   /**
    * The mode for positioning the tooltip
    */
-  position: Scriptable<'average' | 'nearest', ScriptableTooltipContext<TType>>
+  position: Scriptable<TooltipPositioner, ScriptableTooltipContext<TType>>
 
   /**
    * Override the tooltip alignment calculations
@@ -2617,7 +2639,7 @@ export interface TooltipOptions<TType extends ChartType = ChartType> extends Cor
    */
   bodyColor: Scriptable<Color, ScriptableTooltipContext<TType>>;
   /**
-   *   See Fonts.
+   * See Fonts.
    * @default {}
    */
   bodyFont: Scriptable<FontSpec, ScriptableTooltipContext<TType>>;
@@ -2657,7 +2679,7 @@ export interface TooltipOptions<TType extends ChartType = ChartType> extends Cor
    */
   padding: Scriptable<number | ChartArea, ScriptableTooltipContext<TType>>;
   /**
-   *   Extra distance to move the end of the tooltip arrow away from the tooltip point.
+   * Extra distance to move the end of the tooltip arrow away from the tooltip point.
    * @default 2
    */
   caretPadding: Scriptable<number, ScriptableTooltipContext<TType>>;