From 6a63b54ec72c3c293bb1d5ed4abd08e5aec60a31 Mon Sep 17 00:00:00 2001 From: Josh Kelley Date: Thu, 2 Dec 2021 15:32:54 -0500 Subject: [PATCH 1/6] Improve positioner types; allow overriding xAlign and yAlign --- docs/configuration/tooltip.md | 1 + src/plugins/plugin.tooltip.js | 4 ++-- types/index.esm.d.ts | 24 ++++++++++++++++++++---- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/docs/configuration/tooltip.md b/docs/configuration/tooltip.md index 53fd7271635..1516487cafb 100644 --- a/docs/configuration/tooltip.md +++ b/docs/configuration/tooltip.md @@ -81,6 +81,7 @@ tooltipPlugin.positioners.myCustomPositioner = function(elements, eventPosition) return { x: 0, y: 0 + // You may also include xAlign and yAlign to override those tooltip options. }; }; ``` diff --git a/src/plugins/plugin.tooltip.js b/src/plugins/plugin.tooltip.js index 323bafa3a8d..1d05f3002de 100644 --- a/src/plugins/plugin.tooltip.js +++ b/src/plugins/plugin.tooltip.js @@ -256,10 +256,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 }; } diff --git a/types/index.esm.d.ts b/types/index.esm.d.ts index 67c251c6d75..a3ea24ba880 100644 --- a/types/index.esm.d.ts +++ b/types/index.esm.d.ts @@ -2454,9 +2454,25 @@ export interface TooltipModel { setActiveElements(active: ActiveDataPoint[], eventPosition: { x: number, y: number }): void; } +export interface TooltipPosition { + x: number; + y: number; + xAlign?: TooltipXAlignment; + yAlign?: TooltipYAlignment; +} + +export type TooltipPositionFunction = (items: readonly ActiveElement[], eventPosition: { x: number; y: number }) => TooltipPosition | false; + +export interface TooltipPositionerMap { + average: TooltipPositionFunction; + nearest: TooltipPositionFunction; +} + +export type TooltipPositioner = keyof TooltipPositionerMap; + export const Tooltip: Plugin & { readonly positioners: { - [key: string]: (items: readonly ActiveElement[], eventPosition: { x: number; y: number }) => { x: number; y: number } | false; + [key: string]: (items: readonly ActiveElement[], eventPosition: { x: number; y: number }) => TooltipPosition | false; }; }; @@ -2529,7 +2545,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 +2606,7 @@ export interface TooltipOptions extends Cor */ bodyColor: Scriptable>; /** - * See Fonts. + * See Fonts. * @default {} */ bodyFont: Scriptable>; @@ -2630,7 +2646,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>; From 9de760e13683bcdec9f0d056bf5bfe73e8d2b59b Mon Sep 17 00:00:00 2001 From: Josh Kelley Date: Thu, 2 Dec 2021 15:37:53 -0500 Subject: [PATCH 2/6] More type improvements; pass in Chart as third parameter --- docs/configuration/tooltip.md | 3 ++- src/plugins/plugin.tooltip.js | 6 +++--- types/index.esm.d.ts | 16 +++++++++------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/docs/configuration/tooltip.md b/docs/configuration/tooltip.md index 1516487cafb..1f1ef7dad1b 100644 --- a/docs/configuration/tooltip.md +++ b/docs/configuration/tooltip.md @@ -69,10 +69,11 @@ Example: * @function Tooltip.positioners.myCustomPositioner * @param elements {Chart.Element[]} the tooltip elements * @param eventPosition {Point} the position of the event in canvas coordinates + * @param chart {Chart} the chart * @returns {Point} the tooltip position */ const tooltipPlugin = Chart.registry.getPlugin('tooltip'); -tooltipPlugin.positioners.myCustomPositioner = function(elements, eventPosition) { +tooltipPlugin.positioners.myCustomPositioner = function(elements, eventPosition, chart) { /** @type {Tooltip} */ const tooltip = this; diff --git a/src/plugins/plugin.tooltip.js b/src/plugins/plugin.tooltip.js index 1d05f3002de..b8015240e6e 100644 --- a/src/plugins/plugin.tooltip.js +++ b/src/plugins/plugin.tooltip.js @@ -531,7 +531,7 @@ export class Tooltip extends Element { }; } } else { - const position = positioners[options.position].call(this, active, this._eventPosition); + const position = positioners[options.position].call(this, active, this._eventPosition, this._chart); tooltipItems = this._createItems(options); this.title = this.getTitle(tooltipItems, options); @@ -892,7 +892,7 @@ export class Tooltip extends Element { const animX = anims && anims.x; const animY = anims && anims.y; if (animX || animY) { - const position = positioners[options.position].call(this, this._active, this._eventPosition); + const position = positioners[options.position].call(this, this._active, this._eventPosition, chart); if (!position) { return; } @@ -1057,7 +1057,7 @@ export class Tooltip extends Element { */ _positionChanged(active, e) { const {caretX, caretY, options} = this; - const position = positioners[options.position].call(this, active, e); + const position = positioners[options.position].call(this, active, e, this._chart); return position !== false && (caretX !== position.x || caretY !== position.y); } } diff --git a/types/index.esm.d.ts b/types/index.esm.d.ts index a3ea24ba880..1cb74e8e679 100644 --- a/types/index.esm.d.ts +++ b/types/index.esm.d.ts @@ -2451,7 +2451,7 @@ export interface TooltipModel { options: TooltipOptions; getActiveElements(): ActiveElement[]; - setActiveElements(active: ActiveDataPoint[], eventPosition: { x: number, y: number }): void; + setActiveElements(active: ActiveDataPoint[], eventPosition: Point): void; } export interface TooltipPosition { @@ -2461,19 +2461,21 @@ export interface TooltipPosition { yAlign?: TooltipYAlignment; } -export type TooltipPositionFunction = (items: readonly ActiveElement[], eventPosition: { x: number; y: number }) => TooltipPosition | false; +export type TooltipPositionerFunction = ( + items: readonly ActiveElement[], + eventPosition: Point, + chart: Chart +) => TooltipPosition | false; export interface TooltipPositionerMap { - average: TooltipPositionFunction; - nearest: TooltipPositionFunction; + average: TooltipPositionerFunction; + nearest: TooltipPositionerFunction; } export type TooltipPositioner = keyof TooltipPositionerMap; export const Tooltip: Plugin & { - readonly positioners: { - [key: string]: (items: readonly ActiveElement[], eventPosition: { x: number; y: number }) => TooltipPosition | false; - }; + readonly positioners: TooltipPositionerMap; }; export interface TooltipCallbacks< From a2702bedcf71c461f196c92de08472102e5cdc6f Mon Sep 17 00:00:00 2001 From: Josh Kelley Date: Thu, 2 Dec 2021 18:30:02 -0500 Subject: [PATCH 3/6] 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. --- docs/samples/tooltip/position.md | 4 +++- src/plugins/plugin.tooltip.js | 38 +++++++++++++++++------------- test/specs/plugin.tooltip.tests.js | 2 +- types/index.esm.d.ts | 20 +++++++++------- 4 files changed, 37 insertions(+), 27 deletions(-) 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 b8015240e6e..e39c289ebe7 100644 --- a/src/plugins/plugin.tooltip.js +++ b/src/plugins/plugin.tooltip.js @@ -8,6 +8,7 @@ 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 */ @@ -110,6 +111,7 @@ function splitNewlines(str) { /** * Private helper to create a tooltip item model + * @param {Chart} chart * @param item - {element, index, datasetIndex} to create the tooltip item for * @return new tooltip item */ @@ -135,7 +137,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); @@ -353,13 +355,13 @@ 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; + this.chart = config.chart; this.options = config.options; this.dataPoints = undefined; this.title = undefined; @@ -380,6 +382,8 @@ export class Tooltip extends Element { this.labelColors = undefined; this.labelPointStyles = undefined; this.labelTextColors = undefined; + // TODO: V4, remove this backward compatibility alias + this._chart = this.chart; } initialize(options) { @@ -398,10 +402,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 +418,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 +486,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 +494,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 @@ -531,7 +535,7 @@ export class Tooltip extends Element { }; } } else { - const position = positioners[options.position].call(this, active, this._eventPosition, this._chart); + const position = positioners[options.position].call(this, active, this._eventPosition); tooltipItems = this._createItems(options); this.title = this.getTitle(tooltipItems, options); @@ -542,8 +546,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 +571,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,12 +891,12 @@ 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; if (animX || animY) { - const position = positioners[options.position].call(this, this._active, this._eventPosition, chart); + const position = positioners[options.position].call(this, this._active, this._eventPosition); if (!position) { return; } @@ -981,7 +985,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 +1021,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(); } @@ -1057,7 +1061,7 @@ export class Tooltip extends Element { */ _positionChanged(active, e) { const {caretX, caretY, options} = this; - const position = positioners[options.position].call(this, active, e, this._chart); + const position = positioners[options.position].call(this, active, e); return position !== false && (caretX !== position.x || caretY !== position.y); } } @@ -1074,7 +1078,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 1cb74e8e679..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[]; @@ -2461,22 +2463,24 @@ export interface TooltipPosition { yAlign?: TooltipYAlignment; } -export type TooltipPositionerFunction = ( +export type TooltipPositionerFunction = ( + this: TooltipModel, items: readonly ActiveElement[], - eventPosition: Point, - chart: Chart + eventPosition: Point ) => TooltipPosition | false; export interface TooltipPositionerMap { - average: TooltipPositionerFunction; - nearest: TooltipPositionerFunction; + average: TooltipPositionerFunction; + nearest: TooltipPositionerFunction; } export type TooltipPositioner = keyof TooltipPositionerMap; -export const Tooltip: Plugin & { +export interface Tooltip extends Plugin { readonly positioners: TooltipPositionerMap; -}; +} + +export const Tooltip: Tooltip; export interface TooltipCallbacks< TType extends ChartType, From 8205195f89becd7698519ead1e8d5beddb1a7720 Mon Sep 17 00:00:00 2001 From: Josh Kelley Date: Thu, 2 Dec 2021 19:05:08 -0500 Subject: [PATCH 4/6] Update documentation --- docs/configuration/tooltip.md | 87 ++++++++++++++++++++++++----------- 1 file changed, 59 insertions(+), 28 deletions(-) diff --git a/docs/configuration/tooltip.md b/docs/configuration/tooltip.md index 1f1ef7dad1b..27fd6ec88f4 100644 --- a/docs/configuration/tooltip.md +++ b/docs/configuration/tooltip.md @@ -59,33 +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 - * @param chart {Chart} the chart - * @returns {Point} the tooltip position - */ -const tooltipPlugin = Chart.registry.getPlugin('tooltip'); -tooltipPlugin.positioners.myCustomPositioner = function(elements, eventPosition, chart) { - /** @type {Tooltip} */ - const tooltip = this; - - /* ... */ - - return { - x: 0, - y: 0 - // You may also include xAlign and yAlign to override those tooltip options. - }; -}; -``` +You can also define [custom position modes](#custom-position-modes). ### Tooltip Alignment @@ -365,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[], @@ -409,6 +385,61 @@ 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 + * @param chart {Chart} the chart + * @returns {TooltipPosition} the tooltip position + */ +Tooltip.positioners.myCustomPositioner = function(elements, eventPosition, chart) { + // 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 From 8e845b80ae97f3f2ba0d35ea0f070a8bf83cd2b7 Mon Sep 17 00:00:00 2001 From: Josh Kelley Date: Fri, 3 Dec 2021 16:43:39 -0500 Subject: [PATCH 5/6] Fix documentation --- docs/configuration/tooltip.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/configuration/tooltip.md b/docs/configuration/tooltip.md index 27fd6ec88f4..645b2151eac 100644 --- a/docs/configuration/tooltip.md +++ b/docs/configuration/tooltip.md @@ -403,7 +403,6 @@ import { Tooltip } from 'chart.js'; * @function Tooltip.positioners.myCustomPositioner * @param elements {Chart.Element[]} the tooltip elements * @param eventPosition {Point} the position of the event in canvas coordinates - * @param chart {Chart} the chart * @returns {TooltipPosition} the tooltip position */ Tooltip.positioners.myCustomPositioner = function(elements, eventPosition, chart) { From 489b45411837120abd2173c5869d38e0b3ef8ea0 Mon Sep 17 00:00:00 2001 From: Josh Kelley Date: Sun, 5 Dec 2021 17:30:22 -0500 Subject: [PATCH 6/6] Fix issues from code review --- docs/configuration/tooltip.md | 2 +- src/plugins/plugin.tooltip.js | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/configuration/tooltip.md b/docs/configuration/tooltip.md index 645b2151eac..a89de490125 100644 --- a/docs/configuration/tooltip.md +++ b/docs/configuration/tooltip.md @@ -405,7 +405,7 @@ import { Tooltip } from 'chart.js'; * @param eventPosition {Point} the position of the event in canvas coordinates * @returns {TooltipPosition} the tooltip position */ -Tooltip.positioners.myCustomPositioner = function(elements, eventPosition, chart) { +Tooltip.positioners.myCustomPositioner = function(elements, eventPosition) { // A reference to the tooltip model const tooltip = this; diff --git a/src/plugins/plugin.tooltip.js b/src/plugins/plugin.tooltip.js index e39c289ebe7..6d91c7ac8f9 100644 --- a/src/plugins/plugin.tooltip.js +++ b/src/plugins/plugin.tooltip.js @@ -10,6 +10,7 @@ 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 = { @@ -112,7 +113,7 @@ function splitNewlines(str) { /** * Private helper to create a tooltip item model * @param {Chart} chart - * @param item - {element, index, datasetIndex} to create the tooltip item for + * @param {ActiveElement} item - {element, index, datasetIndex} to create the tooltip item for * @return new tooltip item */ function createTooltipItem(chart, item) { @@ -361,7 +362,9 @@ export class Tooltip extends Element { this._tooltipItems = []; this.$animations = undefined; this.$context = undefined; - this.chart = config.chart; + // 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; @@ -382,8 +385,6 @@ export class Tooltip extends Element { this.labelColors = undefined; this.labelPointStyles = undefined; this.labelTextColors = undefined; - // TODO: V4, remove this backward compatibility alias - this._chart = this.chart; } initialize(options) {