Skip to content

Commit

Permalink
Improvements to tooltip positioners (#9944)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
joshkel committed Dec 6, 2021
1 parent 957ca83 commit d83f046
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 55 deletions.
84 changes: 58 additions & 26 deletions docs/configuration/tooltip.md
Expand Up @@ -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

Expand Down Expand Up @@ -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[],

Expand Down Expand Up @@ -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>;
}
}
```
4 changes: 3 additions & 1 deletion docs/samples/tooltip/position.md
Expand Up @@ -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',
};
};

Expand Down
39 changes: 22 additions & 17 deletions src/plugins/plugin.tooltip.js
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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
};
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand All @@ -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) {
Expand Down Expand Up @@ -482,15 +487,15 @@ 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 = [];
let tooltipItems = [];
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
Expand Down Expand Up @@ -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;
Expand All @@ -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});
}
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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});
}
},

Expand Down
2 changes: 1 addition & 1 deletion test/specs/plugin.tooltip.tests.js
Expand Up @@ -1419,7 +1419,7 @@ describe('Plugin.Tooltip', function() {

var mockContext = window.createMockContext();
var tooltip = new Tooltip({
_chart: {
chart: {
getContext: () => ({}),
options: {
plugins: {
Expand Down
42 changes: 32 additions & 10 deletions types/index.esm.d.ts
Expand Up @@ -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>[];

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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>>;
Expand Down Expand Up @@ -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>>;
Expand Down

0 comments on commit d83f046

Please sign in to comment.