]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
feat: Draw tooltips with point styles. Closes #7774 (#7972)
authorDan Manastireanu <498419+danmana@users.noreply.github.com>
Thu, 29 Oct 2020 20:55:40 +0000 (22:55 +0200)
committerGitHub <noreply@github.com>
Thu, 29 Oct 2020 20:55:40 +0000 (22:55 +0200)
* feat: Draw tooltips with point styles. Closes #7774

* chore: Add tooltip usePointStyle docs

* chore: Add tests and visual tests for tooltip usePointStyle

* chore: Update typescript with tooltip usePointStyle

docs/docs/configuration/tooltip.md
samples/samples.js
samples/tooltips/point-style.html [new file with mode: 0644]
src/plugins/plugin.tooltip.js
test/fixtures/core.tooltip/point-style.js [new file with mode: 0644]
test/fixtures/core.tooltip/point-style.png [new file with mode: 0644]
test/specs/plugin.tooltip.tests.js
types/plugins/index.d.ts

index 56f1bbe6771e79c0e327b0c42d6840047aed21a7..aef329af8021f1d0f6ef4251148a694e99827f11 100644 (file)
@@ -37,6 +37,7 @@ The tooltip configuration is passed into the `options.tooltips` namespace. The g
 | `displayColors` | `boolean` | `true` | If true, color boxes are shown in the tooltip.
 | `boxWidth` | `number` | `bodyFont.size` | Width of the color box if displayColors is true.
 | `boxHeight` | `number` | `bodyFont.size` | Height of the color box if displayColors is true.
+| `usePointStyle` | `boolean` | `false` | Use the corresponding point style (from dataset options) instead of color boxes, ex: star, triangle etc. (size is based on the minimum value between boxWidth and boxHeight).
 | `borderColor` | `Color` | `'rgba(0, 0, 0, 0)'` | Color of the border.
 | `borderWidth` | `number` | `0` | Size of the border.
 | `rtl` | `boolean` | | `true` for rendering the legends from right to left.
@@ -111,6 +112,7 @@ All functions are called with the same arguments: a [tooltip item context](#tool
 | `label` | `TooltipItem, object` | Returns text to render for an individual item in the tooltip. [more...](#label-callback)
 | `labelColor` | `TooltipItem, Chart` | Returns the colors to render for the tooltip item. [more...](#label-color-callback)
 | `labelTextColor` | `TooltipItem, Chart` | Returns the colors for the text of the label for the tooltip item.
+| `labelPointStyle` | `TooltipItem, Chart` | Returns the point style to use instead of color boxes if usePointStyle is true (object with values `pointStyle` and `rotation`). Default implementation uses the point style from the dataset points. [more...](#label-point-style-callback)
 | `afterLabel` | `TooltipItem, object` | Returns text to render after an individual label.
 | `afterBody` | `TooltipItem[], object` | Returns text to render after the body section.
 | `beforeFooter` | `TooltipItem[], object` | Returns text to render before the footer section.
@@ -171,6 +173,30 @@ var chart = new Chart(ctx, {
 });
 ```
 
+### Label Point Style Callback
+
+For example, to draw triangles instead of the regular color box for each item in the tooltip you could do:
+
+```javascript
+var chart = new Chart(ctx, {
+    type: 'line',
+    data: data,
+    options: {
+        tooltips: {
+            usePointStyle: true,
+            callbacks: {
+                labelPointStyle: function(context) {
+                    return {
+                        pointStyle: 'triangle',
+                        rotation: 0
+                    };
+                }
+            }
+        }
+    }
+});
+```
+
 
 ### Tooltip Item Context
 
index eb569df52e71b62c374c30e7cd2c9f3f1f75b5ab..a5025f1f09ba277e1d8301db2aa3014de05c980e 100644 (file)
                }, {
                        title: 'Border',
                        path: 'tooltips/border.html'
+               }, {
+                       title: 'Point style',
+                       path: 'tooltips/point-style.html'
                }, {
                        title: 'HTML tooltips (line)',
                        path: 'tooltips/custom-line.html'
diff --git a/samples/tooltips/point-style.html b/samples/tooltips/point-style.html
new file mode 100644 (file)
index 0000000..6ae1376
--- /dev/null
@@ -0,0 +1,193 @@
+<!doctype html>
+<html>
+
+<head>
+       <title>Tooltip Point Style</title>
+       <script src="../../dist/chart.min.js"></script>
+       <script src="../utils.js"></script>
+       <style>
+       canvas{
+               -moz-user-select: none;
+               -webkit-user-select: none;
+               -ms-user-select: none;
+       }
+       </style>
+</head>
+
+<body>
+       <div style="width:75%;">
+               <canvas id="canvas"></canvas>
+       </div>
+       <br>
+       <br>
+       <button id="randomizeData">Randomize Data</button>
+       <button id="addDataset">Add Dataset</button>
+       <button id="removeDataset">Remove Dataset</button>
+       <button id="addData">Add Data</button>
+       <button id="removeData">Remove Data</button>
+       <script>
+               var MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
+               var config = {
+                       type: 'line',
+                       data: {
+                               labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
+                               datasets: [{
+                                       label: 'Triangles',
+                                       backgroundColor: window.chartColors.red,
+                                       borderColor: window.chartColors.red,
+                                       pointStyle: 'triangle',
+                                       pointRadius: 6,
+                                       data: [
+                                               randomScalingFactor(),
+                                               randomScalingFactor(),
+                                               randomScalingFactor(),
+                                               randomScalingFactor(),
+                                               randomScalingFactor(),
+                                               randomScalingFactor(),
+                                               randomScalingFactor()
+                                       ],
+                                       fill: false,
+                               }, {
+                                       label: 'Circles',
+                                       fill: false,
+                                       backgroundColor: window.chartColors.blue,
+                                       borderColor: window.chartColors.blue,
+                                       pointStyle: 'circle',
+                                       pointRadius: 6,
+                                       data: [
+                                               randomScalingFactor(),
+                                               randomScalingFactor(),
+                                               randomScalingFactor(),
+                                               randomScalingFactor(),
+                                               randomScalingFactor(),
+                                               randomScalingFactor(),
+                                               randomScalingFactor()
+                                       ],
+                               }, {
+                                       label: 'Stars',
+                                       fill: false,
+                                       backgroundColor: window.chartColors.green,
+                                       borderColor: window.chartColors.green,
+                                       pointStyle: 'star',
+                                       pointRadius: 6,
+                                       data: [
+                                               randomScalingFactor(),
+                                               randomScalingFactor(),
+                                               randomScalingFactor(),
+                                               randomScalingFactor(),
+                                               randomScalingFactor(),
+                                               randomScalingFactor(),
+                                               randomScalingFactor()
+                                       ],
+                               }]
+                       },
+                       options: {
+                               responsive: true,
+                               title: {
+                                       display: true,
+                                       text: 'Tooltip Point Styles'
+                               },
+                               tooltips: {
+                                       mode: 'index',
+                                       intersect: false,
+                                       usePointStyle: true,
+                               },
+                               legend: {
+                                       labels: {
+                                               usePointStyle: true
+                                       }
+                               },
+                               hover: {
+                                       mode: 'nearest',
+                                       intersect: true
+                               },
+                               scales: {
+                                       x: {
+                                               display: true,
+                                               scaleLabel: {
+                                                       display: true,
+                                                       labelString: 'Month'
+                                               }
+                                       },
+                                       y: {
+                                               display: true,
+                                               scaleLabel: {
+                                                       display: true,
+                                                       labelString: 'Value'
+                                               }
+                                       }
+                               }
+                       }
+               };
+
+               window.onload = function() {
+                       var ctx = document.getElementById('canvas').getContext('2d');
+                       window.myLine = new Chart(ctx, config);
+               };
+
+               document.getElementById('randomizeData').addEventListener('click', function() {
+                       config.data.datasets.forEach(function(dataset) {
+                               dataset.data = dataset.data.map(function() {
+                                       return randomScalingFactor();
+                               });
+
+                       });
+
+                       window.myLine.update();
+               });
+
+               var colorNames = Object.keys(window.chartColors);
+               var pointStyles = ['circle', 'triangle', 'rectRounded', 'rect', 'rectRot', 'cross', 'star', 'line', 'dash'];
+               document.getElementById('addDataset').addEventListener('click', function() {
+                       var colorName = colorNames[config.data.datasets.length % colorNames.length];
+                       var newColor = window.chartColors[colorName];
+                       var newPointStyle = pointStyles[Math.floor(Math.random() * pointStyles.length)];
+                       var newDataset = {
+                               label: 'Dataset ' + config.data.datasets.length,
+                               backgroundColor: newColor,
+                               borderColor: newColor,
+                               pointStyle: newPointStyle,
+                               pointRadius: 6,
+                               data: [],
+                               fill: false
+                       };
+
+                       for (var index = 0; index < config.data.labels.length; ++index) {
+                               newDataset.data.push(randomScalingFactor());
+                       }
+
+                       config.data.datasets.push(newDataset);
+                       window.myLine.update();
+               });
+
+               document.getElementById('addData').addEventListener('click', function() {
+                       if (config.data.datasets.length > 0) {
+                               var month = MONTHS[config.data.labels.length % MONTHS.length];
+                               config.data.labels.push(month);
+
+                               config.data.datasets.forEach(function(dataset) {
+                                       dataset.data.push(randomScalingFactor());
+                               });
+
+                               window.myLine.update();
+                       }
+               });
+
+               document.getElementById('removeDataset').addEventListener('click', function() {
+                       config.data.datasets.splice(0, 1);
+                       window.myLine.update();
+               });
+
+               document.getElementById('removeData').addEventListener('click', function() {
+                       config.data.labels.splice(-1, 1); // remove the label first
+
+                       config.data.datasets.forEach(function(dataset) {
+                               dataset.data.pop();
+                       });
+
+                       window.myLine.update();
+               });
+       </script>
+</body>
+
+</html>
index d9da0d8098ec0ee4dc95a0c96585649cc3bfb288..eac3e5123b57116bebfb0fdbb8ad42e5eed8c7d1 100644 (file)
@@ -5,6 +5,7 @@ import {valueOrDefault, each, noop, isNullOrUndef, isArray, _elementsEqual, merg
 import {getRtlAdapter, overrideTextDirection, restoreTextDirection} from '../helpers/helpers.rtl';
 import {distanceBetweenPoints} from '../helpers/helpers.math';
 import {toFont} from '../helpers/helpers.options';
+import {drawPoint} from '../helpers';
 
 /**
  * @typedef { import("../platform/platform.base").IEvent } IEvent
@@ -382,6 +383,7 @@ export class Tooltip extends Element {
                this.caretX = undefined;
                this.caretY = undefined;
                this.labelColors = undefined;
+               this.labelPointStyles = undefined;
                this.labelTextColors = undefined;
 
                this.initialize();
@@ -485,6 +487,7 @@ export class Tooltip extends Element {
                const options = me.options;
                const data = me._chart.data;
                const labelColors = [];
+               const labelPointStyles = [];
                const labelTextColors = [];
                let tooltipItems = [];
                let i, len;
@@ -506,10 +509,12 @@ export class Tooltip extends Element {
                // Determine colors for boxes
                each(tooltipItems, (context) => {
                        labelColors.push(options.callbacks.labelColor.call(me, context));
+                       labelPointStyles.push(options.callbacks.labelPointStyle.call(me, context));
                        labelTextColors.push(options.callbacks.labelTextColor.call(me, context));
                });
 
                me.labelColors = labelColors;
+               me.labelPointStyles = labelPointStyles;
                me.labelTextColors = labelTextColors;
                me.dataPoints = tooltipItems;
                return tooltipItems;
@@ -668,24 +673,48 @@ export class Tooltip extends Element {
                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 rtlColorX = rtlHelper.x(colorX);
                const yOffSet = boxHeight < bodyFont.size ? (bodyFont.size - boxHeight) / 2 : 0;
                const colorY = pt.y + yOffSet;
 
-               // 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.strokeStyle = labelColors.borderColor;
-               ctx.strokeRect(rtlHelper.leftForLtr(rtlColorX, boxWidth), colorY, boxWidth, boxHeight);
-
-               // Inner square
-               ctx.fillStyle = labelColors.backgroundColor;
-               ctx.fillRect(rtlHelper.leftForLtr(rtlHelper.xPlus(rtlColorX, 1), boxWidth - 2), colorY + 1, boxWidth - 2, boxHeight - 2);
+               if (options.usePointStyle) {
+                       const drawOptions = {
+                               radius: Math.min(boxWidth, boxHeight) / 2, // fit the circle in the box
+                               pointStyle: labelPointStyle.pointStyle,
+                               rotation: labelPointStyle.rotation,
+                               borderWidth: 1
+                       };
+                       // Recalculate x and y for drawPoint() because its expecting
+                       // x and y to be center of figure (instead of top left)
+                       const centerX = rtlHelper.leftForLtr(rtlColorX, boxWidth) + boxWidth / 2;
+                       const centerY = colorY + boxHeight / 2;
+
+                       // Fill the point with white so that colours merge nicely if the opacity is < 1
+                       ctx.strokeStyle = options.multiKeyBackground;
+                       ctx.fillStyle = options.multiKeyBackground;
+                       drawPoint(ctx, drawOptions, centerX, centerY);
+
+                       // Draw the point
+                       ctx.strokeStyle = labelColors.borderColor;
+                       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.strokeStyle = labelColors.borderColor;
+                       ctx.strokeRect(rtlHelper.leftForLtr(rtlColorX, boxWidth), colorY, boxWidth, boxHeight);
+
+                       // Inner square
+                       ctx.fillStyle = labelColors.backgroundColor;
+                       ctx.fillRect(rtlHelper.leftForLtr(rtlHelper.xPlus(rtlColorX, 1), boxWidth - 2), colorY + 1, boxWidth - 2, boxHeight - 2);
+               }
 
                // restore fillStyle
                ctx.fillStyle = me.labelTextColors[i];
@@ -1155,6 +1184,14 @@ export default {
                        labelTextColor() {
                                return this.options.bodyFont.color;
                        },
+                       labelPointStyle(tooltipItem) {
+                               const meta = tooltipItem.chart.getDatasetMeta(tooltipItem.datasetIndex);
+                               const options = meta.controller.getStyle(tooltipItem.dataIndex);
+                               return {
+                                       pointStyle: options.pointStyle,
+                                       rotation: options.rotation,
+                               };
+                       },
                        afterLabel: noop,
 
                        // Args are: (tooltipItems, data)
diff --git a/test/fixtures/core.tooltip/point-style.js b/test/fixtures/core.tooltip/point-style.js
new file mode 100644 (file)
index 0000000..2579f39
--- /dev/null
@@ -0,0 +1,73 @@
+const pointStyles = ['circle', 'cross', 'crossRot', 'dash', 'line', 'rect', 'rectRounded', 'rectRot', 'star', 'triangle'];
+
+function newDataset(pointStyle, i) {
+       return {
+               label: '',
+               data: pointStyles.map(() => i),
+               pointStyle: pointStyle,
+               pointBackgroundColor: '#0000ff',
+               pointBorderColor: '#00ff00',
+               showLine: false
+       };
+}
+module.exports = {
+       config: {
+               type: 'line',
+               data: {
+                       datasets: pointStyles.map((pointStyle, i) => newDataset(pointStyle, i)),
+                       labels: pointStyles.map(() => '')
+               },
+               options: {
+                       legend: false,
+                       title: false,
+                       scales: {
+                               x: {display: false},
+                               y: {display: false}
+                       },
+                       elements: {
+                               line: {
+                                       fill: false
+                               }
+                       },
+                       tooltips: {
+                               mode: 'nearest',
+                               intersect: false,
+                               usePointStyle: true,
+                               callbacks: {
+                                       label: function() {
+                                               return '\u200b';
+                                       }
+                               }
+                       },
+                       layout: {
+                               padding: 15
+                       }
+               },
+               plugins: [{
+                       afterDraw: function(chart) {
+                               var canvas = chart.canvas;
+                               var rect = canvas.getBoundingClientRect();
+                               var point, event;
+
+                               for (var i = 0; i < pointStyles.length; ++i) {
+                                       point = chart.getDatasetMeta(i).data[i];
+                                       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/core.tooltip/point-style.png b/test/fixtures/core.tooltip/point-style.png
new file mode 100644 (file)
index 0000000..defb033
Binary files /dev/null and b/test/fixtures/core.tooltip/point-style.png differ
index 2e5f3cf8ea57dd0b703f4193df64b2312ca2ae3c..b3ba80fc1b300da66a8f2723c5109478fa325fcb 100644 (file)
@@ -369,6 +369,12 @@ describe('Plugin.Tooltip', function() {
                                                },
                                                labelTextColor: function() {
                                                        return 'labelTextColor';
+                                               },
+                                               labelPointStyle: function() {
+                                                       return {
+                                                               pointStyle: 'labelPointStyle',
+                                                               rotation: 42
+                                                       };
                                                }
                                        }
                                }
@@ -459,6 +465,13 @@ describe('Plugin.Tooltip', function() {
                                }, {
                                        borderColor: defaults.color,
                                        backgroundColor: defaults.color
+                               }],
+                               labelPointStyles: [{
+                                       pointStyle: 'labelPointStyle',
+                                       rotation: 42
+                               }, {
+                                       pointStyle: 'labelPointStyle',
+                                       rotation: 42
                                }]
                        }));
 
index e5855fe880f1d32ebffd2b05cffb89ec1fd446ee..514ff06a8de6ee9a2fee57f401fcc4b63cbf3f7b 100644 (file)
@@ -281,6 +281,7 @@ export interface TooltipModel {
   // colors to render for each item in body[]. This is the color of the squares in the tooltip
   labelColors: Color[];
   labelTextColors: Color[];
+  labelPointStyles: { pointStyle: PointStyle; rotation: number }[];
 
   // 0 opacity is a hidden tooltip
   opacity: number;
@@ -312,6 +313,7 @@ export interface ITooltipCallbacks {
 
   labelColor(this: TooltipModel, tooltipItem: ITooltipItem): { borderColor: Color; backgroundColor: Color };
   labelTextColor(this: TooltipModel, tooltipItem: ITooltipItem): Color;
+  labelPointStyle(this: TooltipModel, tooltipItem: ITooltipItem): { pointStyle: PointStyle; rotation: number };
 
   beforeFooter(this: TooltipModel, tooltipItems: ITooltipItem[]): string | string[];
   footer(this: TooltipModel, tooltipItems: ITooltipItem[]): string | string[];
@@ -473,6 +475,11 @@ export interface ITooltipOptions extends IHoverInteractionOptions {
    * @default bodyFont.size
    */
   boxHeight: number;
+  /**
+   * Use the corresponding point style (from dataset options) instead of color boxes, ex: star, triangle etc. (size is based on the minimum value between boxWidth and boxHeight)
+   * @default false
+   */
+  usePointStyle: boolean;
   /**
    * Color of the border.
    * @default 'rgba(0, 0, 0, 0)'