diff --git a/docs/docs/configuration/tooltip.md b/docs/docs/configuration/tooltip.md index 56f1bbe6771..aef329af802 100644 --- a/docs/docs/configuration/tooltip.md +++ b/docs/docs/configuration/tooltip.md @@ -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 diff --git a/samples/samples.js b/samples/samples.js index eb569df52e7..a5025f1f09b 100644 --- a/samples/samples.js +++ b/samples/samples.js @@ -205,6 +205,9 @@ }, { 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 index 00000000000..6ae1376c4d0 --- /dev/null +++ b/samples/tooltips/point-style.html @@ -0,0 +1,193 @@ + + + + + Tooltip Point Style + + + + + + +
+ +
+
+
+ + + + + + + + + diff --git a/src/plugins/plugin.tooltip.js b/src/plugins/plugin.tooltip.js index d9da0d8098e..eac3e5123b5 100644 --- a/src/plugins/plugin.tooltip.js +++ b/src/plugins/plugin.tooltip.js @@ -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 index 00000000000..2579f395948 --- /dev/null +++ b/test/fixtures/core.tooltip/point-style.js @@ -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 index 00000000000..defb03359fc Binary files /dev/null and b/test/fixtures/core.tooltip/point-style.png differ diff --git a/test/specs/plugin.tooltip.tests.js b/test/specs/plugin.tooltip.tests.js index 2e5f3cf8ea5..b3ba80fc1b3 100644 --- a/test/specs/plugin.tooltip.tests.js +++ b/test/specs/plugin.tooltip.tests.js @@ -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 }] })); diff --git a/types/plugins/index.d.ts b/types/plugins/index.d.ts index e5855fe880f..514ff06a8de 100644 --- a/types/plugins/index.d.ts +++ b/types/plugins/index.d.ts @@ -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)'