Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements to tooltip positioners #9944

Merged
merged 6 commits into from Dec 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -2402,7 +2402,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 @@ -2451,14 +2453,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 @@ -2529,7 +2551,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 @@ -2590,7 +2612,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 @@ -2630,7 +2652,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