Skip to content

Commit d83f046

Browse files
authored
Improvements to tooltip positioners (#9944)
* 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
1 parent 957ca83 commit d83f046

File tree

5 files changed

+116
-55
lines changed

5 files changed

+116
-55
lines changed

docs/configuration/tooltip.md

Lines changed: 58 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -59,31 +59,7 @@ Possible modes are:
5959

6060
`'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.
6161

62-
New modes can be defined by adding functions to the `Chart.Tooltip.positioners` map.
63-
64-
Example:
65-
66-
```javascript
67-
/**
68-
* Custom positioner
69-
* @function Tooltip.positioners.myCustomPositioner
70-
* @param elements {Chart.Element[]} the tooltip elements
71-
* @param eventPosition {Point} the position of the event in canvas coordinates
72-
* @returns {Point} the tooltip position
73-
*/
74-
const tooltipPlugin = Chart.registry.getPlugin('tooltip');
75-
tooltipPlugin.positioners.myCustomPositioner = function(elements, eventPosition) {
76-
/** @type {Tooltip} */
77-
const tooltip = this;
78-
79-
/* ... */
80-
81-
return {
82-
x: 0,
83-
y: 0
84-
};
85-
};
86-
```
62+
You can also define [custom position modes](#custom-position-modes).
8763

8864
### Tooltip Alignment
8965

@@ -363,6 +339,8 @@ The tooltip model contains parameters that can be used to render the tooltip.
363339

364340
```javascript
365341
{
342+
chart: Chart,
343+
366344
// The items that we are rendering in the tooltip. See Tooltip Item Interface section
367345
dataPoints: TooltipItem[],
368346

@@ -407,6 +385,60 @@ The tooltip model contains parameters that can be used to render the tooltip.
407385
opacity: number,
408386

409387
// tooltip options
410-
options : Object
388+
options: Object
411389
}
412390
```
391+
392+
## Custom Position Modes
393+
394+
New modes can be defined by adding functions to the `Chart.Tooltip.positioners` map.
395+
396+
Example:
397+
398+
```javascript
399+
import { Tooltip } from 'chart.js';
400+
401+
/**
402+
* Custom positioner
403+
* @function Tooltip.positioners.myCustomPositioner
404+
* @param elements {Chart.Element[]} the tooltip elements
405+
* @param eventPosition {Point} the position of the event in canvas coordinates
406+
* @returns {TooltipPosition} the tooltip position
407+
*/
408+
Tooltip.positioners.myCustomPositioner = function(elements, eventPosition) {
409+
// A reference to the tooltip model
410+
const tooltip = this;
411+
412+
/* ... */
413+
414+
return {
415+
x: 0,
416+
y: 0
417+
// You may also include xAlign and yAlign to override those tooltip options.
418+
};
419+
};
420+
421+
// Then, to use it...
422+
new Chart.js(ctx, {
423+
data,
424+
options: {
425+
plugins: {
426+
tooltip: {
427+
position: 'myCustomPositioner'
428+
}
429+
}
430+
}
431+
})
432+
```
433+
434+
See [samples](/samples/tooltip/position.md) for a more detailed example.
435+
436+
If you're using TypeScript, you'll also need to register the new mode:
437+
438+
```typescript
439+
declare module 'chart.js' {
440+
interface TooltipPositionerMap {
441+
myCustomPositioner: TooltipPositionerFunction<ChartType>;
442+
}
443+
}
444+
```

docs/samples/tooltip/position.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,13 @@ components.Tooltip.positioners.bottom = function(items) {
6363
return false;
6464
}
6565

66-
const chart = this._chart;
66+
const chart = this.chart;
6767

6868
return {
6969
x: pos.x,
7070
y: chart.chartArea.bottom,
71+
xAlign: 'center',
72+
yAlign: 'bottom',
7173
};
7274
};
7375

src/plugins/plugin.tooltip.js

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import {distanceBetweenPoints, _limitValue} from '../helpers/helpers.math';
88
import {createContext, drawPoint} from '../helpers';
99

1010
/**
11+
* @typedef { import("../platform/platform.base").Chart } Chart
1112
* @typedef { import("../platform/platform.base").ChartEvent } ChartEvent
13+
* @typedef { import("../../types/index.esm").ActiveElement } ActiveElement
1214
*/
1315

1416
const positioners = {
@@ -110,7 +112,8 @@ function splitNewlines(str) {
110112

111113
/**
112114
* Private helper to create a tooltip item model
113-
* @param item - {element, index, datasetIndex} to create the tooltip item for
115+
* @param {Chart} chart
116+
* @param {ActiveElement} item - {element, index, datasetIndex} to create the tooltip item for
114117
* @return new tooltip item
115118
*/
116119
function createTooltipItem(chart, item) {
@@ -135,7 +138,7 @@ function createTooltipItem(chart, item) {
135138
* Get the size of the tooltip
136139
*/
137140
function getTooltipSize(tooltip, options) {
138-
const ctx = tooltip._chart.ctx;
141+
const ctx = tooltip.chart.ctx;
139142
const {body, footer, title} = tooltip;
140143
const {boxWidth, boxHeight} = options;
141144
const bodyFont = toFont(options.bodyFont);
@@ -256,10 +259,10 @@ function determineXAlign(chart, options, size, yAlign) {
256259
* Helper to get the alignment of a tooltip given the size
257260
*/
258261
function determineAlignment(chart, options, size) {
259-
const yAlign = options.yAlign || determineYAlign(chart, size);
262+
const yAlign = size.yAlign || options.yAlign || determineYAlign(chart, size);
260263

261264
return {
262-
xAlign: options.xAlign || determineXAlign(chart, options, size, yAlign),
265+
xAlign: size.xAlign || options.xAlign || determineXAlign(chart, options, size, yAlign),
263266
yAlign
264267
};
265268
}
@@ -353,13 +356,15 @@ export class Tooltip extends Element {
353356

354357
this.opacity = 0;
355358
this._active = [];
356-
this._chart = config._chart;
357359
this._eventPosition = undefined;
358360
this._size = undefined;
359361
this._cachedAnimations = undefined;
360362
this._tooltipItems = [];
361363
this.$animations = undefined;
362364
this.$context = undefined;
365+
// TODO: V4, remove config._chart and this._chart backward compatibility aliases
366+
this.chart = config.chart || config._chart;
367+
this._chart = this.chart;
363368
this.options = config.options;
364369
this.dataPoints = undefined;
365370
this.title = undefined;
@@ -398,10 +403,10 @@ export class Tooltip extends Element {
398403
return cached;
399404
}
400405

401-
const chart = this._chart;
406+
const chart = this.chart;
402407
const options = this.options.setContext(this.getContext());
403408
const opts = options.enabled && chart.options.animation && options.animations;
404-
const animations = new Animations(this._chart, opts);
409+
const animations = new Animations(this.chart, opts);
405410
if (opts._cacheable) {
406411
this._cachedAnimations = Object.freeze(animations);
407412
}
@@ -414,7 +419,7 @@ export class Tooltip extends Element {
414419
*/
415420
getContext() {
416421
return this.$context ||
417-
(this.$context = createTooltipContext(this._chart.getContext(), this, this._tooltipItems));
422+
(this.$context = createTooltipContext(this.chart.getContext(), this, this._tooltipItems));
418423
}
419424

420425
getTitle(context, options) {
@@ -482,15 +487,15 @@ export class Tooltip extends Element {
482487
*/
483488
_createItems(options) {
484489
const active = this._active;
485-
const data = this._chart.data;
490+
const data = this.chart.data;
486491
const labelColors = [];
487492
const labelPointStyles = [];
488493
const labelTextColors = [];
489494
let tooltipItems = [];
490495
let i, len;
491496

492497
for (i = 0, len = active.length; i < len; ++i) {
493-
tooltipItems.push(createTooltipItem(this._chart, active[i]));
498+
tooltipItems.push(createTooltipItem(this.chart, active[i]));
494499
}
495500

496501
// If the user provided a filter function, use it to modify the tooltip items
@@ -542,8 +547,8 @@ export class Tooltip extends Element {
542547

543548
const size = this._size = getTooltipSize(this, options);
544549
const positionAndSize = Object.assign({}, position, size);
545-
const alignment = determineAlignment(this._chart, options, positionAndSize);
546-
const backgroundPoint = getBackgroundPoint(options, positionAndSize, alignment, this._chart);
550+
const alignment = determineAlignment(this.chart, options, positionAndSize);
551+
const backgroundPoint = getBackgroundPoint(options, positionAndSize, alignment, this.chart);
547552

548553
this.xAlign = alignment.xAlign;
549554
this.yAlign = alignment.yAlign;
@@ -567,7 +572,7 @@ export class Tooltip extends Element {
567572
}
568573

569574
if (changed && options.external) {
570-
options.external.call(this, {chart: this._chart, tooltip: this, replay});
575+
options.external.call(this, {chart: this.chart, tooltip: this, replay});
571576
}
572577
}
573578

@@ -887,7 +892,7 @@ export class Tooltip extends Element {
887892
* @private
888893
*/
889894
_updateAnimationTarget(options) {
890-
const chart = this._chart;
895+
const chart = this.chart;
891896
const anims = this.$animations;
892897
const animX = anims && anims.x;
893898
const animY = anims && anims.y;
@@ -981,7 +986,7 @@ export class Tooltip extends Element {
981986
setActiveElements(activeElements, eventPosition) {
982987
const lastActive = this._active;
983988
const active = activeElements.map(({datasetIndex, index}) => {
984-
const meta = this._chart.getDatasetMeta(datasetIndex);
989+
const meta = this.chart.getDatasetMeta(datasetIndex);
985990

986991
if (!meta) {
987992
throw new Error('Cannot find a dataset at index ' + datasetIndex);
@@ -1017,7 +1022,7 @@ export class Tooltip extends Element {
10171022

10181023
// Find Active Elements for tooltips
10191024
if (e.type !== 'mouseout') {
1020-
active = this._chart.getElementsAtEventForMode(e, options.mode, options, replay);
1025+
active = this.chart.getElementsAtEventForMode(e, options.mode, options, replay);
10211026
if (options.reverse) {
10221027
active.reverse();
10231028
}
@@ -1074,7 +1079,7 @@ export default {
10741079

10751080
afterInit(chart, _args, options) {
10761081
if (options) {
1077-
chart.tooltip = new Tooltip({_chart: chart, options});
1082+
chart.tooltip = new Tooltip({chart, options});
10781083
}
10791084
},
10801085

test/specs/plugin.tooltip.tests.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1419,7 +1419,7 @@ describe('Plugin.Tooltip', function() {
14191419

14201420
var mockContext = window.createMockContext();
14211421
var tooltip = new Tooltip({
1422-
_chart: {
1422+
chart: {
14231423
getContext: () => ({}),
14241424
options: {
14251425
plugins: {

types/index.esm.d.ts

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2429,7 +2429,9 @@ export interface TooltipLabelStyle {
24292429
*/
24302430
borderRadius?: number | BorderRadius;
24312431
}
2432-
export interface TooltipModel<TType extends ChartType> {
2432+
export interface TooltipModel<TType extends ChartType> extends Element<AnyObject, TooltipOptions<TType>> {
2433+
readonly chart: Chart<TType>;
2434+
24332435
// The items that we are rendering in the tooltip. See Tooltip Item Interface section
24342436
dataPoints: TooltipItem<TType>[];
24352437

@@ -2478,14 +2480,34 @@ export interface TooltipModel<TType extends ChartType> {
24782480
options: TooltipOptions<TType>;
24792481

24802482
getActiveElements(): ActiveElement[];
2481-
setActiveElements(active: ActiveDataPoint[], eventPosition: { x: number, y: number }): void;
2483+
setActiveElements(active: ActiveDataPoint[], eventPosition: Point): void;
24822484
}
24832485

2484-
export const Tooltip: Plugin & {
2485-
readonly positioners: {
2486-
[key: string]: (items: readonly ActiveElement[], eventPosition: { x: number; y: number }) => { x: number; y: number } | false;
2487-
};
2488-
};
2486+
export interface TooltipPosition {
2487+
x: number;
2488+
y: number;
2489+
xAlign?: TooltipXAlignment;
2490+
yAlign?: TooltipYAlignment;
2491+
}
2492+
2493+
export type TooltipPositionerFunction<TType extends ChartType> = (
2494+
this: TooltipModel<TType>,
2495+
items: readonly ActiveElement[],
2496+
eventPosition: Point
2497+
) => TooltipPosition | false;
2498+
2499+
export interface TooltipPositionerMap {
2500+
average: TooltipPositionerFunction<ChartType>;
2501+
nearest: TooltipPositionerFunction<ChartType>;
2502+
}
2503+
2504+
export type TooltipPositioner = keyof TooltipPositionerMap;
2505+
2506+
export interface Tooltip extends Plugin {
2507+
readonly positioners: TooltipPositionerMap;
2508+
}
2509+
2510+
export const Tooltip: Tooltip;
24892511

24902512
export interface TooltipCallbacks<
24912513
TType extends ChartType,
@@ -2556,7 +2578,7 @@ export interface TooltipOptions<TType extends ChartType = ChartType> extends Cor
25562578
/**
25572579
* The mode for positioning the tooltip
25582580
*/
2559-
position: Scriptable<'average' | 'nearest', ScriptableTooltipContext<TType>>
2581+
position: Scriptable<TooltipPositioner, ScriptableTooltipContext<TType>>
25602582

25612583
/**
25622584
* Override the tooltip alignment calculations
@@ -2617,7 +2639,7 @@ export interface TooltipOptions<TType extends ChartType = ChartType> extends Cor
26172639
*/
26182640
bodyColor: Scriptable<Color, ScriptableTooltipContext<TType>>;
26192641
/**
2620-
* See Fonts.
2642+
* See Fonts.
26212643
* @default {}
26222644
*/
26232645
bodyFont: Scriptable<FontSpec, ScriptableTooltipContext<TType>>;
@@ -2657,7 +2679,7 @@ export interface TooltipOptions<TType extends ChartType = ChartType> extends Cor
26572679
*/
26582680
padding: Scriptable<number | ChartArea, ScriptableTooltipContext<TType>>;
26592681
/**
2660-
* Extra distance to move the end of the tooltip arrow away from the tooltip point.
2682+
* Extra distance to move the end of the tooltip arrow away from the tooltip point.
26612683
* @default 2
26622684
*/
26632685
caretPadding: Scriptable<number, ScriptableTooltipContext<TType>>;

0 commit comments

Comments
 (0)