diff --git a/docs/configuration/tooltip.md b/docs/configuration/tooltip.md index 53fd7271635..a89de490125 100644 --- a/docs/configuration/tooltip.md +++ b/docs/configuration/tooltip.md @@ -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; + } +} +``` \ No newline at end of file diff --git a/docs/samples/tooltip/position.md b/docs/samples/tooltip/position.md index fba8eaa3615..c2cd854ef8d 100644 --- a/docs/samples/tooltip/position.md +++ b/docs/samples/tooltip/position.md @@ -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', }; }; diff --git a/src/plugins/plugin.tooltip.js b/src/plugins/plugin.tooltip.js index 323bafa3a8d..6d91c7ac8f9 100644 --- a/src/plugins/plugin.tooltip.js +++ b/src/plugins/plugin.tooltip.js @@ -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}); } }, diff --git a/test/specs/plugin.tooltip.tests.js b/test/specs/plugin.tooltip.tests.js index ef0945d9537..d181c870ca1 100644 --- a/test/specs/plugin.tooltip.tests.js +++ b/test/specs/plugin.tooltip.tests.js @@ -1419,7 +1419,7 @@ describe('Plugin.Tooltip', function() { var mockContext = window.createMockContext(); var tooltip = new Tooltip({ - _chart: { + chart: { getContext: () => ({}), options: { plugins: { diff --git a/types/index.esm.d.ts b/types/index.esm.d.ts index 67c251c6d75..6ae9f85d03f 100644 --- a/types/index.esm.d.ts +++ b/types/index.esm.d.ts @@ -2402,7 +2402,9 @@ export interface TooltipLabelStyle { */ borderRadius?: number | BorderRadius; } -export interface TooltipModel { +export interface TooltipModel extends Element> { + readonly chart: Chart; + // The items that we are rendering in the tooltip. See Tooltip Item Interface section dataPoints: TooltipItem[]; @@ -2451,14 +2453,34 @@ export interface TooltipModel { options: TooltipOptions; 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 = ( + this: TooltipModel, + items: readonly ActiveElement[], + eventPosition: Point +) => TooltipPosition | false; + +export interface TooltipPositionerMap { + average: TooltipPositionerFunction; + nearest: TooltipPositionerFunction; +} + +export type TooltipPositioner = keyof TooltipPositionerMap; + +export interface Tooltip extends Plugin { + readonly positioners: TooltipPositionerMap; +} + +export const Tooltip: Tooltip; export interface TooltipCallbacks< TType extends ChartType, @@ -2529,7 +2551,7 @@ export interface TooltipOptions extends Cor /** * The mode for positioning the tooltip */ - position: Scriptable<'average' | 'nearest', ScriptableTooltipContext> + position: Scriptable> /** * Override the tooltip alignment calculations @@ -2590,7 +2612,7 @@ export interface TooltipOptions extends Cor */ bodyColor: Scriptable>; /** - * See Fonts. + * See Fonts. * @default {} */ bodyFont: Scriptable>; @@ -2630,7 +2652,7 @@ export interface TooltipOptions extends Cor */ padding: Scriptable>; /** - * 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>;