Skip to content

Commit

Permalink
feat: Draw tooltips with point styles. Closes chartjs#7774 (chartjs#7972
Browse files Browse the repository at this point in the history
)

* feat: Draw tooltips with point styles. Closes chartjs#7774

* chore: Add tooltip usePointStyle docs

* chore: Add tests and visual tests for tooltip usePointStyle

* chore: Update typescript with tooltip usePointStyle
  • Loading branch information
danmana authored Oct 29, 2020
1 parent aad748d commit 6869a41
Showing 8 changed files with 364 additions and 12 deletions.
26 changes: 26 additions & 0 deletions docs/docs/configuration/tooltip.md
Original file line number Diff line number Diff line change
@@ -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

3 changes: 3 additions & 0 deletions samples/samples.js
Original file line number Diff line number Diff line change
@@ -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'
193 changes: 193 additions & 0 deletions samples/tooltips/point-style.html
Original file line number Diff line number Diff line change
@@ -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>
61 changes: 49 additions & 12 deletions src/plugins/plugin.tooltip.js
Original file line number Diff line number Diff line change
@@ -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)
Loading

0 comments on commit 6869a41

Please sign in to comment.