mirror of
https://github.com/chartjs/Chart.js.git
synced 2026-03-06 16:26:52 +01:00
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:
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
75
test/fixtures/plugin.tooltip/color-box-border-dash.js
vendored
Normal file
75
test/fixtures/plugin.tooltip/color-box-border-dash.js
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
};
|
||||
BIN
test/fixtures/plugin.tooltip/color-box-border-dash.png
vendored
Normal file
BIN
test/fixtures/plugin.tooltip/color-box-border-dash.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
78
test/fixtures/plugin.tooltip/color-box-border-radius.js
vendored
Normal file
78
test/fixtures/plugin.tooltip/color-box-border-radius.js
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
};
|
||||
BIN
test/fixtures/plugin.tooltip/color-box-border-radius.png
vendored
Normal file
BIN
test/fixtures/plugin.tooltip/color-box-border-radius.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
@@ -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
33
types/index.esm.d.ts
vendored
@@ -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 };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user