Skip to content

Commit

Permalink
Tooltip specific scriptable context (#8561)
Browse files Browse the repository at this point in the history
* Update plugin to use it's own tooltip context

* Scriptable tooltip option types

* Tests

* Update types to use UnionToIntersection

* Update TooltipItem to use UnionToIntersection
  • Loading branch information
etimberg authored Mar 4, 2021
1 parent 3e551d5 commit 91628c1
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 92 deletions.
133 changes: 75 additions & 58 deletions src/plugins/plugin.tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,9 @@ function createTooltipItem(chart, item) {
/**
* Get the size of the tooltip
*/
function getTooltipSize(tooltip) {
function getTooltipSize(tooltip, options) {
const ctx = tooltip._chart.ctx;
const {body, footer, options, title} = tooltip;
const {body, footer, title} = tooltip;
const {bodyFont, footerFont, titleFont, boxWidth, boxHeight} = options;
const titleLineCount = title.length;
const footerLineCount = footer.length;
Expand Down Expand Up @@ -322,8 +322,7 @@ function getBackgroundPoint(options, size, alignment, chart) {
return {x, y};
}

function getAlignedX(tooltip, align) {
const options = tooltip.options;
function getAlignedX(tooltip, align, options) {
const padding = toPadding(options.padding);

return align === 'center'
Expand All @@ -340,6 +339,14 @@ function getBeforeAfterBodyLines(callback) {
return pushOrConcat([], splitNewlines(callback));
}

function createTooltipContext(parent, tooltip, tooltipItems) {
return Object.assign(Object.create(parent), {
tooltip,
tooltipItems,
type: 'tooltip'
});
}

export class Tooltip extends Element {
constructor(config) {
super();
Expand All @@ -350,7 +357,9 @@ export class Tooltip extends Element {
this._eventPosition = undefined;
this._size = undefined;
this._cachedAnimations = undefined;
this._tooltipItems = [];
this.$animations = undefined;
this.$context = undefined;
this.options = config.options;
this.dataPoints = undefined;
this.title = undefined;
Expand All @@ -374,6 +383,7 @@ export class Tooltip extends Element {
initialize(options) {
this.options = options;
this._cachedAnimations = undefined;
this.$context = undefined;
}

/**
Expand All @@ -388,7 +398,7 @@ export class Tooltip extends Element {
}

const chart = me._chart;
const options = me.options;
const options = me.options.setContext(me.getContext());
const opts = options.enabled && chart.options.animation && options.animations;
const animations = new Animations(me._chart, opts);
if (opts._cacheable) {
Expand All @@ -398,10 +408,18 @@ export class Tooltip extends Element {
return animations;
}

getTitle(context) {
/**
* @protected
*/
getContext() {
const me = this;
return me.$context ||
(me.$context = createTooltipContext(me._chart.getContext(), me, me._tooltipItems));
}

getTitle(context, options) {
const me = this;
const opts = me.options;
const callbacks = opts.callbacks;
const {callbacks} = options;

const beforeTitle = callbacks.beforeTitle.apply(me, [context]);
const title = callbacks.title.apply(me, [context]);
Expand All @@ -415,13 +433,13 @@ export class Tooltip extends Element {
return lines;
}

getBeforeBody(tooltipItems) {
return getBeforeAfterBodyLines(this.options.callbacks.beforeBody.apply(this, [tooltipItems]));
getBeforeBody(tooltipItems, options) {
return getBeforeAfterBodyLines(options.callbacks.beforeBody.apply(this, [tooltipItems]));
}

getBody(tooltipItems) {
getBody(tooltipItems, options) {
const me = this;
const callbacks = me.options.callbacks;
const {callbacks} = options;
const bodyItems = [];

each(tooltipItems, (context) => {
Expand All @@ -440,14 +458,14 @@ export class Tooltip extends Element {
return bodyItems;
}

getAfterBody(tooltipItems) {
return getBeforeAfterBodyLines(this.options.callbacks.afterBody.apply(this, [tooltipItems]));
getAfterBody(tooltipItems, options) {
return getBeforeAfterBodyLines(options.callbacks.afterBody.apply(this, [tooltipItems]));
}

// Get the footer and beforeFooter and afterFooter lines
getFooter(tooltipItems) {
getFooter(tooltipItems, options) {
const me = this;
const callbacks = me.options.callbacks;
const {callbacks} = options;

const beforeFooter = callbacks.beforeFooter.apply(me, [tooltipItems]);
const footer = callbacks.footer.apply(me, [tooltipItems]);
Expand All @@ -464,10 +482,9 @@ export class Tooltip extends Element {
/**
* @private
*/
_createItems() {
_createItems(options) {
const me = this;
const active = me._active;
const options = me.options;
const data = me._chart.data;
const labelColors = [];
const labelPointStyles = [];
Expand Down Expand Up @@ -505,9 +522,10 @@ export class Tooltip extends Element {

update(changed) {
const me = this;
const options = me.options;
const options = me.options.setContext(me.getContext());
const active = me._active;
let properties;
let tooltipItems = [];

if (!active.length) {
if (me.opacity !== 0) {
Expand All @@ -517,15 +535,15 @@ export class Tooltip extends Element {
}
} else {
const position = positioners[options.position].call(me, active, me._eventPosition);
const tooltipItems = me._createItems();
tooltipItems = me._createItems(options);

me.title = me.getTitle(tooltipItems);
me.beforeBody = me.getBeforeBody(tooltipItems);
me.body = me.getBody(tooltipItems);
me.afterBody = me.getAfterBody(tooltipItems);
me.footer = me.getFooter(tooltipItems);
me.title = me.getTitle(tooltipItems, options);
me.beforeBody = me.getBeforeBody(tooltipItems, options);
me.body = me.getBody(tooltipItems, options);
me.afterBody = me.getAfterBody(tooltipItems, options);
me.footer = me.getFooter(tooltipItems, options);

const size = me._size = getTooltipSize(me);
const size = me._size = getTooltipSize(me, options);
const positionAndSize = Object.assign({}, position, size);
const alignment = determineAlignment(me._chart, options, positionAndSize);
const backgroundPoint = getBackgroundPoint(options, positionAndSize, alignment, me._chart);
Expand All @@ -544,6 +562,9 @@ export class Tooltip extends Element {
};
}

me._tooltipItems = tooltipItems;
me.$context = undefined;

if (properties) {
me._resolveAnimations().update(me, properties);
}
Expand All @@ -553,16 +574,16 @@ export class Tooltip extends Element {
}
}

drawCaret(tooltipPoint, ctx, size) {
const caretPosition = this.getCaretPosition(tooltipPoint, size);
drawCaret(tooltipPoint, ctx, size, options) {
const caretPosition = this.getCaretPosition(tooltipPoint, size, options);

ctx.lineTo(caretPosition.x1, caretPosition.y1);
ctx.lineTo(caretPosition.x2, caretPosition.y2);
ctx.lineTo(caretPosition.x3, caretPosition.y3);
}

getCaretPosition(tooltipPoint, size) {
const {xAlign, yAlign, options} = this;
getCaretPosition(tooltipPoint, size, options) {
const {xAlign, yAlign} = this;
const {cornerRadius, caretSize} = options;
const {x: ptX, y: ptY} = tooltipPoint;
const {width, height} = size;
Expand Down Expand Up @@ -617,17 +638,16 @@ export class Tooltip extends Element {
return {x1, x2, x3, y1, y2, y3};
}

drawTitle(pt, ctx) {
drawTitle(pt, ctx, options) {
const me = this;
const options = me.options;
const title = me.title;
const length = title.length;
let titleFont, titleSpacing, i;

if (length) {
const rtlHelper = getRtlAdapter(options.rtl, me.x, me.width);

pt.x = getAlignedX(me, options.titleAlign);
pt.x = getAlignedX(me, options.titleAlign, options);

ctx.textAlign = rtlHelper.textAlign(options.titleAlign);
ctx.textBaseline = 'middle';
Expand All @@ -652,13 +672,12 @@ export class Tooltip extends Element {
/**
* @private
*/
_drawColorBox(ctx, pt, i, rtlHelper) {
_drawColorBox(ctx, pt, i, rtlHelper, options) {
const me = this;
const options = me.options;
const labelColors = me.labelColors[i];
const labelPointStyle = me.labelPointStyles[i];
const {boxHeight, boxWidth, bodyFont} = options;
const colorX = getAlignedX(me, 'left');
const colorX = getAlignedX(me, 'left', options);
const rtlColorX = rtlHelper.x(colorX);
const yOffSet = boxHeight < bodyFont.size ? (bodyFont.size - boxHeight) / 2 : 0;
const colorY = pt.y + yOffSet;
Expand Down Expand Up @@ -703,9 +722,9 @@ export class Tooltip extends Element {
ctx.fillStyle = me.labelTextColors[i];
}

drawBody(pt, ctx) {
drawBody(pt, ctx, options) {
const me = this;
const {body, options} = me;
const {body} = me;
const {bodyFont, bodySpacing, bodyAlign, displayColors, boxHeight, boxWidth} = options;
let bodyLineHeight = bodyFont.size;
let xLinePadding = 0;
Expand All @@ -724,7 +743,7 @@ export class Tooltip extends Element {
ctx.textBaseline = 'middle';
ctx.font = toFontString(bodyFont);

pt.x = getAlignedX(me, bodyAlignForCalculation);
pt.x = getAlignedX(me, bodyAlignForCalculation, options);

// Before body lines
ctx.fillStyle = options.bodyColor;
Expand All @@ -745,7 +764,7 @@ export class Tooltip extends Element {
lines = bodyItem.lines;
// Draw Legend-like boxes if needed
if (displayColors && lines.length) {
me._drawColorBox(ctx, pt, i, rtlHelper);
me._drawColorBox(ctx, pt, i, rtlHelper, options);
bodyLineHeight = Math.max(bodyFont.size, boxHeight);
}

Expand All @@ -767,17 +786,16 @@ export class Tooltip extends Element {
pt.y -= bodySpacing; // Remove last body spacing
}

drawFooter(pt, ctx) {
drawFooter(pt, ctx, options) {
const me = this;
const options = me.options;
const footer = me.footer;
const length = footer.length;
let footerFont, i;

if (length) {
const rtlHelper = getRtlAdapter(options.rtl, me.x, me.width);

pt.x = getAlignedX(me, options.footerAlign);
pt.x = getAlignedX(me, options.footerAlign, options);
pt.y += options.footerMarginTop;

ctx.textAlign = rtlHelper.textAlign(options.footerAlign);
Expand All @@ -795,8 +813,8 @@ export class Tooltip extends Element {
}
}

drawBackground(pt, ctx, tooltipSize) {
const {xAlign, yAlign, options} = this;
drawBackground(pt, ctx, tooltipSize, options) {
const {xAlign, yAlign} = this;
const {x, y} = pt;
const {width, height} = tooltipSize;
const radius = options.cornerRadius;
Expand All @@ -808,22 +826,22 @@ export class Tooltip extends Element {
ctx.beginPath();
ctx.moveTo(x + radius, y);
if (yAlign === 'top') {
this.drawCaret(pt, ctx, tooltipSize);
this.drawCaret(pt, ctx, tooltipSize, options);
}
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
if (yAlign === 'center' && xAlign === 'right') {
this.drawCaret(pt, ctx, tooltipSize);
this.drawCaret(pt, ctx, tooltipSize, options);
}
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
if (yAlign === 'bottom') {
this.drawCaret(pt, ctx, tooltipSize);
this.drawCaret(pt, ctx, tooltipSize, options);
}
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
if (yAlign === 'center' && xAlign === 'left') {
this.drawCaret(pt, ctx, tooltipSize);
this.drawCaret(pt, ctx, tooltipSize, options);
}
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
Expand All @@ -840,10 +858,9 @@ export class Tooltip extends Element {
* Update x/y animation targets when _active elements are animating too
* @private
*/
_updateAnimationTarget() {
_updateAnimationTarget(options) {
const me = this;
const chart = me._chart;
const options = me.options;
const anims = me.$animations;
const animX = anims && anims.x;
const animY = anims && anims.y;
Expand All @@ -852,7 +869,7 @@ export class Tooltip extends Element {
if (!position) {
return;
}
const size = me._size = getTooltipSize(me);
const size = me._size = getTooltipSize(me, options);
const positionAndSize = Object.assign({}, position, me._size);
const alignment = determineAlignment(chart, options, positionAndSize);
const point = getBackgroundPoint(options, positionAndSize, alignment, chart);
Expand All @@ -870,14 +887,14 @@ export class Tooltip extends Element {

draw(ctx) {
const me = this;
const options = me.options;
const options = me.options.setContext(me.getContext());
let opacity = me.opacity;

if (!opacity) {
return;
}

me._updateAnimationTarget();
me._updateAnimationTarget(options);

const tooltipSize = {
width: me.width,
Expand All @@ -901,20 +918,20 @@ export class Tooltip extends Element {
ctx.globalAlpha = opacity;

// Draw Background
me.drawBackground(pt, ctx, tooltipSize);
me.drawBackground(pt, ctx, tooltipSize, options);

overrideTextDirection(ctx, options.textDirection);

pt.y += padding.top;

// Titles
me.drawTitle(pt, ctx);
me.drawTitle(pt, ctx, options);

// Body
me.drawBody(pt, ctx);
me.drawBody(pt, ctx, options);

// Footer
me.drawFooter(pt, ctx);
me.drawFooter(pt, ctx, options);

restoreTextDirection(ctx, options.textDirection);

Expand Down
Loading

0 comments on commit 91628c1

Please sign in to comment.