From: Evert Timberg Date: Sat, 10 Apr 2021 17:37:22 +0000 (-0400) Subject: Tooltip colorbox supports configurable borderWidth, borderRadius, and dash effect... X-Git-Tag: v3.1.0~6 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=7ee498e4124599b22b2b15fa13399baab5a9ccf0;p=thirdparty%2FChart.js.git 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 --- diff --git a/docs/configuration/tooltip.md b/docs/configuration/tooltip.md index 1375f50cb..145f083d9 100644 --- a/docs/configuration/tooltip.md +++ b/docs/configuration/tooltip.md @@ -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) { diff --git a/src/elements/element.bar.js b/src/elements/element.bar.js index 3e2860ffd..8d9006804 100644 --- a/src/elements/element.bar.js +++ b/src/elements/element.bar.js @@ -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 diff --git a/src/helpers/helpers.canvas.js b/src/helpers/helpers.canvas.js index 8282b3a94..a23dce715 100644 --- a/src/helpers/helpers.canvas.js +++ b/src/helpers/helpers.canvas.js @@ -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); +} diff --git a/src/plugins/plugin.tooltip.js b/src/plugins/plugin.tooltip.js index 7b59d9493..419a8b876 100644 --- a/src/plugins/plugin.tooltip.js +++ b/src/plugins/plugin.tooltip.js @@ -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() { diff --git a/test/fixtures/plugin.tooltip/color-box-border-dash.js b/test/fixtures/plugin.tooltip/color-box-border-dash.js new file mode 100644 index 000000000..b24472a68 --- /dev/null +++ b/test/fixtures/plugin.tooltip/color-box-border-dash.js @@ -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 + } + } +}; diff --git a/test/fixtures/plugin.tooltip/color-box-border-dash.png b/test/fixtures/plugin.tooltip/color-box-border-dash.png new file mode 100644 index 000000000..8eb6868f8 Binary files /dev/null and b/test/fixtures/plugin.tooltip/color-box-border-dash.png differ diff --git a/test/fixtures/plugin.tooltip/color-box-border-radius.js b/test/fixtures/plugin.tooltip/color-box-border-radius.js new file mode 100644 index 000000000..170719d84 --- /dev/null +++ b/test/fixtures/plugin.tooltip/color-box-border-radius.js @@ -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 + } + } +}; diff --git a/test/fixtures/plugin.tooltip/color-box-border-radius.png b/test/fixtures/plugin.tooltip/color-box-border-radius.png new file mode 100644 index 000000000..f59f62849 Binary files /dev/null and b/test/fixtures/plugin.tooltip/color-box-border-radius.png differ diff --git a/test/specs/plugin.tooltip.tests.js b/test/specs/plugin.tooltip.tests.js index 20ca9428b..8060514bc 100644 --- a/test/specs/plugin.tooltip.tests.js +++ b/test/specs/plugin.tooltip.tests.js @@ -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, }] })); }); diff --git a/types/index.esm.d.ts b/types/index.esm.d.ts index 01415b273..71f01be03 100644 --- a/types/index.esm.d.ts +++ b/types/index.esm.d.ts @@ -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 { // The items that we are rendering in the tooltip. See Tooltip Item Interface section dataPoints: TooltipItem[]; @@ -2284,8 +2311,8 @@ export interface TooltipModel { // 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 };