]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Tooltip colorbox supports configurable borderWidth, borderRadius, and dash effect...
authorEvert Timberg <evert.timberg+github@gmail.com>
Sat, 10 Apr 2021 17:37:22 +0000 (13:37 -0400)
committerGitHub <noreply@github.com>
Sat, 10 Apr 2021 17:37:22 +0000 (13:37 -0400)
* Start on extending tooltip style
* Correct borderRadius implementation
* Tests of updated tooltip styling
* Update docs

docs/configuration/tooltip.md
src/elements/element.bar.js
src/helpers/helpers.canvas.js
src/plugins/plugin.tooltip.js
test/fixtures/plugin.tooltip/color-box-border-dash.js [new file with mode: 0644]
test/fixtures/plugin.tooltip/color-box-border-dash.png [new file with mode: 0644]
test/fixtures/plugin.tooltip/color-box-border-radius.js [new file with mode: 0644]
test/fixtures/plugin.tooltip/color-box-border-radius.png [new file with mode: 0644]
test/specs/plugin.tooltip.tests.js
types/index.esm.d.ts

index 1375f50cbb3ec98c2965de2420a7bc81f4c44676..145f083d90e030795072e3be73195573849b728d 100644 (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) {
index 3e2860ffd4d02d3f5aafa4d51ce99c4b553f7aa0..8d90068043577ae5b14c0faa5d490c16b4b2dfeb 100644 (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
index 8282b3a94255681a2f310ec5ded741b74bc8595e..a23dce715a510ac71c564fdeaad4bca38313d626 100644 (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);
+}
index 7b59d94938fbc0e03d5613cd2fab2796764e1fc0..419a8b8767cb48c70baa53fd26c4e4c07798338e 100644 (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() {
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 (file)
index 0000000..b24472a
--- /dev/null
@@ -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 (file)
index 0000000..8eb6868
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 (file)
index 0000000..170719d
--- /dev/null
@@ -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 (file)
index 0000000..f59f628
Binary files /dev/null and b/test/fixtures/plugin.tooltip/color-box-border-radius.png differ
index 20ca9428b31fb7c1c11e5dc1cbcfbff317eeb320..8060514bcfc351d488a87a2613adb8f5e12a3332 100644 (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,
       }]
     }));
   });
index 01415b2732f7dea4bbd2ab85fc51193fa4a8a48f..71f01be031b18a10e3985d0adbfb34def6718f7a 100644 (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 };