Tooltip colorbox supports configurable borderWidth, borderRadius, and dash effect (#8874)

* Start on extending tooltip style
* Correct borderRadius implementation
* Tests of updated tooltip styling
* Update docs
This commit is contained in:
Evert Timberg
2021-04-10 13:37:22 -04:00
committed by GitHub
parent a84347b035
commit 7ee498e412
10 changed files with 333 additions and 63 deletions

View File

@@ -153,7 +153,7 @@ var chart = new Chart(ctx, {
### Label Color Callback
For example, to return a red box for each item in the tooltip you could do:
For example, to return a red box with a blue dashed border that has a border radius for each item in the tooltip you could do:
```javascript
var chart = new Chart(ctx, {
@@ -165,8 +165,11 @@ var chart = new Chart(ctx, {
callbacks: {
labelColor: function(context) {
return {
borderColor: 'rgb(255, 0, 0)',
backgroundColor: 'rgb(255, 0, 0)'
borderColor: 'rgb(0, 0, 255)',
backgroundColor: 'rgb(255, 0, 0)',
borderWidth: 2,
borderDash: [2, 2],
borderRadius: 2,
};
},
labelTextColor: function(context) {

View File

@@ -1,6 +1,6 @@
import Element from '../core/core.element';
import {addRoundedRectPath} from '../helpers/helpers.canvas';
import {toTRBL, toTRBLCorners} from '../helpers/helpers.options';
import {PI, HALF_PI} from '../helpers/helpers.math';
/**
* Helper function to get the bounds of the bar regardless of the orientation
@@ -141,39 +141,6 @@ function hasRadius(radius) {
return radius.topLeft || radius.topRight || radius.bottomLeft || radius.bottomRight;
}
/**
* Add a path of a rectangle with rounded corners to the current sub-path
* @param {CanvasRenderingContext2D} ctx Context
* @param {*} rect Bounding rect
*/
function addRoundedRectPath(ctx, rect) {
const {x, y, w, h, radius} = rect;
// top left arc
ctx.arc(x + radius.topLeft, y + radius.topLeft, radius.topLeft, -HALF_PI, PI, true);
// line from top left to bottom left
ctx.lineTo(x, y + h - radius.bottomLeft);
// bottom left arc
ctx.arc(x + radius.bottomLeft, y + h - radius.bottomLeft, radius.bottomLeft, PI, HALF_PI, true);
// line from bottom left to bottom right
ctx.lineTo(x + w - radius.bottomRight, y + h);
// bottom right arc
ctx.arc(x + w - radius.bottomRight, y + h - radius.bottomRight, radius.bottomRight, HALF_PI, 0, true);
// line from bottom right to top right
ctx.lineTo(x + w, y + radius.topRight);
// top right arc
ctx.arc(x + w - radius.topRight, y + radius.topRight, radius.topRight, 0, -HALF_PI, true);
// line from top right to top left
ctx.lineTo(x + radius.topLeft, y);
}
/**
* Add a path of a rectangle to the current sub-path
* @param {CanvasRenderingContext2D} ctx Context

View File

@@ -377,3 +377,36 @@ export function renderText(ctx, text, x, y, font, opts = {}) {
ctx.restore();
}
/**
* Add a path of a rectangle with rounded corners to the current sub-path
* @param {CanvasRenderingContext2D} ctx Context
* @param {*} rect Bounding rect
*/
export function addRoundedRectPath(ctx, rect) {
const {x, y, w, h, radius} = rect;
// top left arc
ctx.arc(x + radius.topLeft, y + radius.topLeft, radius.topLeft, -HALF_PI, PI, true);
// line from top left to bottom left
ctx.lineTo(x, y + h - radius.bottomLeft);
// bottom left arc
ctx.arc(x + radius.bottomLeft, y + h - radius.bottomLeft, radius.bottomLeft, PI, HALF_PI, true);
// line from bottom left to bottom right
ctx.lineTo(x + w - radius.bottomRight, y + h);
// bottom right arc
ctx.arc(x + w - radius.bottomRight, y + h - radius.bottomRight, radius.bottomRight, HALF_PI, 0, true);
// line from bottom right to top right
ctx.lineTo(x + w, y + radius.topRight);
// top right arc
ctx.arc(x + w - radius.topRight, y + radius.topRight, radius.topRight, 0, -HALF_PI, true);
// line from top right to top left
ctx.lineTo(x + radius.topLeft, y);
}

View File

@@ -1,7 +1,8 @@
import Animations from '../core/core.animations';
import Element from '../core/core.element';
import {addRoundedRectPath} from '../helpers/helpers.canvas';
import {each, noop, isNullOrUndef, isArray, _elementsEqual} from '../helpers/helpers.core';
import {toFont, toPadding} from '../helpers/helpers.options';
import {toFont, toPadding, toTRBLCorners} from '../helpers/helpers.options';
import {getRtlAdapter, overrideTextDirection, restoreTextDirection} from '../helpers/helpers.rtl';
import {distanceBetweenPoints, _limitValue} from '../helpers/helpers.math';
import {drawPoint} from '../helpers';
@@ -377,6 +378,8 @@ export class Tooltip extends Element {
this.width = undefined;
this.caretX = undefined;
this.caretY = undefined;
// TODO: V4, make this private, rename to `_labelStyles`, and combine with `labelPointStyles`
// and `labelTextColors` to create a single variable
this.labelColors = undefined;
this.labelPointStyles = undefined;
this.labelTextColors = undefined;
@@ -709,18 +712,50 @@ export class Tooltip extends Element {
ctx.fillStyle = labelColors.backgroundColor;
drawPoint(ctx, drawOptions, centerX, centerY);
} else {
// Fill a white rect so that colours merge nicely if the opacity is < 1
ctx.fillStyle = options.multiKeyBackground;
ctx.fillRect(rtlHelper.leftForLtr(rtlColorX, boxWidth), colorY, boxWidth, boxHeight);
// Border
ctx.lineWidth = 1;
ctx.lineWidth = labelColors.borderWidth || 1; // TODO, v4 remove fallback
ctx.strokeStyle = labelColors.borderColor;
ctx.strokeRect(rtlHelper.leftForLtr(rtlColorX, boxWidth), colorY, boxWidth, boxHeight);
ctx.setLineDash(labelColors.borderDash || []);
ctx.lineDashOffset = labelColors.borderDashOffset || 0;
// Inner square
ctx.fillStyle = labelColors.backgroundColor;
ctx.fillRect(rtlHelper.leftForLtr(rtlHelper.xPlus(rtlColorX, 1), boxWidth - 2), colorY + 1, boxWidth - 2, boxHeight - 2);
// Fill a white rect so that colours merge nicely if the opacity is < 1
const outerX = rtlHelper.leftForLtr(rtlColorX, boxWidth);
const innerX = rtlHelper.leftForLtr(rtlHelper.xPlus(rtlColorX, 1), boxWidth - 2);
const borderRadius = toTRBLCorners(labelColors.borderRadius);
if (Object.values(borderRadius).some(v => v !== 0)) {
ctx.beginPath();
ctx.fillStyle = options.multiKeyBackground;
addRoundedRectPath(ctx, {
x: outerX,
y: colorY,
w: boxWidth,
h: boxHeight,
radius: borderRadius,
});
ctx.fill();
ctx.stroke();
// Inner square
ctx.fillStyle = labelColors.backgroundColor;
ctx.beginPath();
addRoundedRectPath(ctx, {
x: innerX,
y: colorY + 1,
w: boxWidth - 2,
h: boxHeight - 2,
radius: borderRadius,
});
ctx.fill();
} else {
// Normal rect
ctx.fillStyle = options.multiKeyBackground;
ctx.fillRect(outerX, colorY, boxWidth, boxHeight);
ctx.strokeRect(outerX, colorY, boxWidth, boxHeight);
// Inner square
ctx.fillStyle = labelColors.backgroundColor;
ctx.fillRect(innerX, colorY + 1, boxWidth - 2, boxHeight - 2);
}
}
// restore fillStyle
@@ -1197,7 +1232,11 @@ export default {
const options = meta.controller.getStyle(tooltipItem.dataIndex);
return {
borderColor: options.borderColor,
backgroundColor: options.backgroundColor
backgroundColor: options.backgroundColor,
borderWidth: options.borderWidth,
borderDash: options.borderDash,
borderDashOffset: options.borderDashOffset,
borderRadius: 0,
};
},
labelTextColor() {

View File

@@ -0,0 +1,75 @@
module.exports = {
config: {
type: 'line',
data: {
datasets: [{
data: [8, 7, 6, 5],
pointBorderColor: '#ff0000',
pointBackgroundColor: '#00ff00',
showLine: false
}],
labels: ['', '', '', '']
},
options: {
scales: {
x: {display: false},
y: {display: false}
},
elements: {
line: {
fill: false
}
},
plugins: {
legend: false,
title: false,
filler: false,
tooltip: {
mode: 'nearest',
intersect: false,
callbacks: {
label: function() {
return '\u200b';
},
labelColor: function(tooltipItem) {
const meta = tooltipItem.chart.getDatasetMeta(tooltipItem.datasetIndex);
const options = meta.controller.getStyle(tooltipItem.dataIndex);
return {
borderColor: options.borderColor,
backgroundColor: options.backgroundColor,
borderWidth: 2,
borderDash: [2, 2]
};
},
}
},
},
layout: {
padding: 15
}
},
plugins: [{
afterDraw: function(chart) {
const canvas = chart.canvas;
const rect = canvas.getBoundingClientRect();
const point = chart.getDatasetMeta(0).data[1];
const event = {
type: 'mousemove',
target: canvas,
clientX: rect.left + point.x,
clientY: rect.top + point.y
};
chart._handleEvent(event);
chart.tooltip.handleEvent(event);
chart.tooltip.draw(chart.ctx);
}
}]
},
options: {
canvas: {
height: 256,
width: 512
}
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,78 @@
module.exports = {
config: {
type: 'line',
data: {
datasets: [{
data: [8, 7, 6, 5],
pointBorderColor: '#ff0000',
pointBackgroundColor: '#00ff00',
showLine: false
}],
labels: ['', '', '', '']
},
options: {
scales: {
x: {display: false},
y: {display: false}
},
elements: {
line: {
fill: false
}
},
plugins: {
legend: false,
title: false,
filler: false,
tooltip: {
mode: 'nearest',
intersect: false,
callbacks: {
label: function() {
return '\u200b';
},
labelColor: function(tooltipItem) {
const meta = tooltipItem.chart.getDatasetMeta(tooltipItem.datasetIndex);
const options = meta.controller.getStyle(tooltipItem.dataIndex);
return {
borderColor: options.borderColor,
backgroundColor: options.backgroundColor,
borderWidth: 2,
borderRadius: {
topRight: 5,
bottomRight: 5,
},
};
},
}
},
},
layout: {
padding: 15
}
},
plugins: [{
afterDraw: function(chart) {
const canvas = chart.canvas;
const rect = canvas.getBoundingClientRect();
const point = chart.getDatasetMeta(0).data[1];
const event = {
type: 'mousemove',
target: canvas,
clientX: rect.left + point.x,
clientY: rect.top + point.y
};
chart._handleEvent(event);
chart.tooltip.handleEvent(event);
chart.tooltip.draw(chart.ctx);
}
}]
},
options: {
canvas: {
height: 256,
width: 512
}
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -156,10 +156,18 @@ describe('Plugin.Tooltip', function() {
footer: [],
labelColors: [{
borderColor: defaults.borderColor,
backgroundColor: defaults.backgroundColor
backgroundColor: defaults.backgroundColor,
borderWidth: 1,
borderDash: undefined,
borderDashOffset: undefined,
borderRadius: 0,
}, {
borderColor: defaults.borderColor,
backgroundColor: defaults.backgroundColor
backgroundColor: defaults.backgroundColor,
borderWidth: 1,
borderDash: undefined,
borderDashOffset: undefined,
borderRadius: 0,
}]
}));
@@ -307,7 +315,11 @@ describe('Plugin.Tooltip', function() {
expect(tooltip.labelColors).toEqual([{
borderColor: defaults.borderColor,
backgroundColor: defaults.backgroundColor
backgroundColor: defaults.backgroundColor,
borderWidth: 1,
borderDash: undefined,
borderDashOffset: undefined,
borderRadius: 0,
}]);
expect(tooltip.x).toBeCloseToPixel(267);
@@ -460,10 +472,18 @@ describe('Plugin.Tooltip', function() {
labelTextColors: ['labelTextColor', 'labelTextColor'],
labelColors: [{
borderColor: defaults.borderColor,
backgroundColor: defaults.backgroundColor
backgroundColor: defaults.backgroundColor,
borderWidth: 1,
borderDash: undefined,
borderDashOffset: undefined,
borderRadius: 0,
}, {
borderColor: defaults.borderColor,
backgroundColor: defaults.backgroundColor
backgroundColor: defaults.backgroundColor,
borderWidth: 1,
borderDash: undefined,
borderDashOffset: undefined,
borderRadius: 0,
}],
labelPointStyles: [{
pointStyle: 'labelPointStyle',
@@ -573,10 +593,18 @@ describe('Plugin.Tooltip', function() {
footer: [],
labelColors: [{
borderColor: defaults.borderColor,
backgroundColor: defaults.backgroundColor
backgroundColor: defaults.backgroundColor,
borderWidth: 1,
borderDash: undefined,
borderDashOffset: undefined,
borderRadius: 0,
}, {
borderColor: defaults.borderColor,
backgroundColor: defaults.backgroundColor
backgroundColor: defaults.backgroundColor,
borderWidth: 1,
borderDash: undefined,
borderDashOffset: undefined,
borderRadius: 0,
}]
}));
@@ -641,10 +669,18 @@ describe('Plugin.Tooltip', function() {
footer: [],
labelColors: [{
borderColor: defaults.borderColor,
backgroundColor: defaults.backgroundColor
backgroundColor: defaults.backgroundColor,
borderWidth: 1,
borderDash: undefined,
borderDashOffset: undefined,
borderRadius: 0,
}, {
borderColor: defaults.borderColor,
backgroundColor: defaults.backgroundColor
backgroundColor: defaults.backgroundColor,
borderWidth: 1,
borderDash: undefined,
borderDashOffset: undefined,
borderRadius: 0,
}]
}));
@@ -710,10 +746,18 @@ describe('Plugin.Tooltip', function() {
footer: [],
labelColors: [{
borderColor: defaults.borderColor,
backgroundColor: defaults.backgroundColor
backgroundColor: defaults.backgroundColor,
borderWidth: 1,
borderDash: undefined,
borderDashOffset: undefined,
borderRadius: 0,
}, {
borderColor: defaults.borderColor,
backgroundColor: defaults.backgroundColor
backgroundColor: defaults.backgroundColor,
borderWidth: 1,
borderDash: undefined,
borderDashOffset: undefined,
borderRadius: 0,
}]
}));
@@ -778,7 +822,11 @@ describe('Plugin.Tooltip', function() {
footer: [],
labelColors: [{
borderColor: defaults.borderColor,
backgroundColor: defaults.backgroundColor
backgroundColor: defaults.backgroundColor,
borderWidth: 1,
borderDash: undefined,
borderDashOffset: undefined,
borderRadius: 0,
}]
}));
});

33
types/index.esm.d.ts vendored
View File

@@ -2246,7 +2246,34 @@ export interface TitleOptions {
export type TooltipXAlignment = 'left' | 'center' | 'right';
export type TooltipYAlignment = 'top' | 'center' | 'bottom';
export interface TooltipLabelStyle {
borderColor: Color;
backgroundColor: Color;
/**
* Width of border line
* @since 3.1.0
*/
borderWidth?: number;
/**
* Border dash
* @since 3.1.0
*/
borderDash?: [number, number];
/**
* Border dash offset
* @since 3.1.0
*/
borderDashOffset?: number;
/**
* borderRadius
* @since 3.1.0
*/
borderRadius?: number | BorderRadius;
}
export interface TooltipModel<TType extends ChartType> {
// The items that we are rendering in the tooltip. See Tooltip Item Interface section
dataPoints: TooltipItem<TType>[];
@@ -2284,8 +2311,8 @@ export interface TooltipModel<TType extends ChartType> {
// lines of text that form the footer
footer: string[];
// colors to render for each item in body[]. This is the color of the squares in the tooltip
labelColors: Color[];
// Styles to render for each item in body[]. This is the styling of the squares in the tooltip
labelColors: TooltipLabelStyle[];
labelTextColors: Color[];
labelPointStyles: { pointStyle: PointStyle; rotation: number }[];
@@ -2321,7 +2348,7 @@ export interface TooltipCallbacks<
label(this: Model, tooltipItem: Item): string | string[];
afterLabel(this: Model, tooltipItem: Item): string | string[];
labelColor(this: Model, tooltipItem: Item): { borderColor: Color; backgroundColor: Color };
labelColor(this: Model, tooltipItem: Item): TooltipLabelStyle;
labelTextColor(this: Model, tooltipItem: Item): Color;
labelPointStyle(this: Model, tooltipItem: Item): { pointStyle: PointStyle; rotation: number };