Chart.js/src/core/core.tooltip.js

344 lines
11 KiB
JavaScript

(function() {
"use strict";
var root = this,
Chart = root.Chart,
helpers = Chart.helpers;
Chart.defaults.global.tooltips = {
enabled: true,
custom: null,
backgroundColor: "rgba(0,0,0,0.8)",
fontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
fontSize: 10,
fontStyle: "normal",
fontColor: "#fff",
titleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
titleFontSize: 12,
titleFontStyle: "bold",
titleFontColor: "#fff",
yPadding: 6,
xPadding: 6,
caretSize: 8,
cornerRadius: 6,
xOffset: 10,
template: [
'<% if(label){ %>',
'<%=label %>: ',
'<% } %>',
'<%=value %>',
].join(''),
multiTemplate: [
'<%if (datasetLabel){ %>',
'<%=datasetLabel %>: ',
'<% } %>',
'<%=value %>'
].join(''),
multiKeyBackground: '#fff',
};
Chart.Tooltip = Chart.Element.extend({
initialize: function() {
var options = this._options;
helpers.extend(this, {
_model: {
// Positioning
xPadding: options.tooltips.xPadding,
yPadding: options.tooltips.yPadding,
xOffset: options.tooltips.xOffset,
// Labels
textColor: options.tooltips.fontColor,
_fontFamily: options.tooltips.fontFamily,
_fontStyle: options.tooltips.fontStyle,
fontSize: options.tooltips.fontSize,
// Title
titleTextColor: options.tooltips.titleFontColor,
_titleFontFamily: options.tooltips.titleFontFamily,
_titleFontStyle: options.tooltips.titleFontStyle,
titleFontSize: options.tooltips.titleFontSize,
// Appearance
caretHeight: options.tooltips.caretSize,
cornerRadius: options.tooltips.cornerRadius,
backgroundColor: options.tooltips.backgroundColor,
opacity: 0,
legendColorBackground: options.tooltips.multiKeyBackground,
},
});
},
update: function() {
var ctx = this._chart.ctx;
switch (this._options.hover.mode) {
case 'single':
helpers.extend(this._model, {
text: helpers.template(this._options.tooltips.template, {
// These variables are available in the template function. Add others here
element: this._active[0],
value: this._data.datasets[this._active[0]._datasetIndex].data[this._active[0]._index],
label: this._active[0]._model.label !== undefined ? this._active[0]._model.label : this._data.labels ? this._data.labels[this._active[0]._index] : '',
}),
});
var tooltipPosition = this._active[0].tooltipPosition();
helpers.extend(this._model, {
x: Math.round(tooltipPosition.x),
y: Math.round(tooltipPosition.y),
caretPadding: tooltipPosition.padding
});
break;
case 'label':
// Tooltip Content
var dataArray,
dataIndex;
var labels = [],
colors = [];
for (var i = this._data.datasets.length - 1; i >= 0; i--) {
dataArray = this._data.datasets[i].metaData;
dataIndex = helpers.indexOf(dataArray, this._active[0]);
if (dataIndex !== -1) {
break;
}
}
var medianPosition = (function(index) {
// Get all the points at that particular index
var elements = [],
dataCollection,
xPositions = [],
yPositions = [],
xMax,
yMax,
xMin,
yMin;
helpers.each(this._data.datasets, function(dataset) {
dataCollection = dataset.metaData;
if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()) {
elements.push(dataCollection[dataIndex]);
}
}, this);
// Reverse labels if stacked
helpers.each(this._options.stacked ? elements.reverse() : elements, function(element) {
xPositions.push(element._view.x);
yPositions.push(element._view.y);
//Include any colour information about the element
labels.push(helpers.template(this._options.tooltips.multiTemplate, {
// These variables are available in the template function. Add others here
element: element,
datasetLabel: this._data.datasets[element._datasetIndex].label,
value: this._data.datasets[element._datasetIndex].data[element._index],
}));
colors.push({
fill: element._view.backgroundColor,
stroke: element._view.borderColor
});
}, this);
yMin = helpers.min(yPositions);
yMax = helpers.max(yPositions);
xMin = helpers.min(xPositions);
xMax = helpers.max(xPositions);
return {
x: (xMin > this._chart.width / 2) ? xMin : xMax,
y: (yMin + yMax) / 2,
};
}).call(this, dataIndex);
// Apply for now
helpers.extend(this._model, {
x: medianPosition.x,
y: medianPosition.y,
labels: labels,
title: this._data.labels && this._data.labels.length ? this._data.labels[this._active[0]._index] : '',
legendColors: colors,
legendBackgroundColor: this._options.tooltips.multiKeyBackground,
});
// Calculate Appearance Tweaks
this._model.height = (labels.length * this._model.fontSize) + ((labels.length - 1) * (this._model.fontSize / 2)) + (this._model.yPadding * 2) + this._model.titleFontSize * 1.5;
var titleWidth = ctx.measureText(this._model.title).width,
//Label has a legend square as well so account for this.
labelWidth = helpers.longestText(ctx, this.font, labels) + this._model.fontSize + 3,
longestTextWidth = helpers.max([labelWidth, titleWidth]);
this._model.width = longestTextWidth + (this._model.xPadding * 2);
var halfHeight = this._model.height / 2;
//Check to ensure the height will fit on the canvas
if (this._model.y - halfHeight < 0) {
this._model.y = halfHeight;
} else if (this._model.y + halfHeight > this._chart.height) {
this._model.y = this._chart.height - halfHeight;
}
//Decide whether to align left or right based on position on canvas
if (this._model.x > this._chart.width / 2) {
this._model.x -= this._model.xOffset + this._model.width;
} else {
this._model.x += this._model.xOffset;
}
break;
}
return this;
},
draw: function() {
var ctx = this._chart.ctx;
var vm = this._view;
switch (this._options.hover.mode) {
case 'single':
ctx.font = helpers.fontString(vm.fontSize, vm._fontStyle, vm._fontFamily);
vm.xAlign = "center";
vm.yAlign = "above";
//Distance between the actual element.y position and the start of the tooltip caret
var caretPadding = vm.caretPadding || 2;
var tooltipWidth = ctx.measureText(vm.text).width + 2 * vm.xPadding,
tooltipRectHeight = vm.fontSize + 2 * vm.yPadding,
tooltipHeight = tooltipRectHeight + vm.caretHeight + caretPadding;
if (vm.x + tooltipWidth / 2 > this._chart.width) {
vm.xAlign = "left";
} else if (vm.x - tooltipWidth / 2 < 0) {
vm.xAlign = "right";
}
if (vm.y - tooltipHeight < 0) {
vm.yAlign = "below";
}
var tooltipX = vm.x - tooltipWidth / 2,
tooltipY = vm.y - tooltipHeight;
ctx.fillStyle = helpers.color(vm.backgroundColor).alpha(vm.opacity).rgbString();
// Custom Tooltips
if (this._options.tooltips.custom) {
this._options.tooltips.custom(this);
}
if (!this._options.tooltips.enabled) {
return;
}
switch (vm.yAlign) {
case "above":
//Draw a caret above the x/y
ctx.beginPath();
ctx.moveTo(vm.x, vm.y - caretPadding);
ctx.lineTo(vm.x + vm.caretHeight, vm.y - (caretPadding + vm.caretHeight));
ctx.lineTo(vm.x - vm.caretHeight, vm.y - (caretPadding + vm.caretHeight));
ctx.closePath();
ctx.fill();
break;
case "below":
tooltipY = vm.y + caretPadding + vm.caretHeight;
//Draw a caret below the x/y
ctx.beginPath();
ctx.moveTo(vm.x, vm.y + caretPadding);
ctx.lineTo(vm.x + vm.caretHeight, vm.y + caretPadding + vm.caretHeight);
ctx.lineTo(vm.x - vm.caretHeight, vm.y + caretPadding + vm.caretHeight);
ctx.closePath();
ctx.fill();
break;
}
switch (vm.xAlign) {
case "left":
tooltipX = vm.x - tooltipWidth + (vm.cornerRadius + vm.caretHeight);
break;
case "right":
tooltipX = vm.x - (vm.cornerRadius + vm.caretHeight);
break;
}
helpers.drawRoundedRectangle(ctx, tooltipX, tooltipY, tooltipWidth, tooltipRectHeight, vm.cornerRadius);
ctx.fill();
ctx.fillStyle = helpers.color(vm.textColor).alpha(vm.opacity).rgbString();
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(vm.text, tooltipX + tooltipWidth / 2, tooltipY + tooltipRectHeight / 2);
break;
case 'label':
// Custom Tooltips
if (this._options.tooltips.custom) {
this._options.tooltips.custom(this);
}
if (!this._options.tooltips.enabled) {
return;
}
helpers.drawRoundedRectangle(ctx, vm.x, vm.y - vm.height / 2, vm.width, vm.height, vm.cornerRadius);
ctx.fillStyle = helpers.color(vm.backgroundColor).alpha(vm.opacity).rgbString();
ctx.fill();
ctx.closePath();
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.fillStyle = helpers.color(vm.titleTextColor).alpha(vm.opacity).rgbString();
ctx.font = helpers.fontString(vm.fontSize, vm._titleFontStyle, vm._titleFontFamily);
ctx.fillText(vm.title, vm.x + vm.xPadding, this.getLineHeight(0));
ctx.font = helpers.fontString(vm.fontSize, vm._fontStyle, vm._fontFamily);
helpers.each(vm.labels, function(label, index) {
ctx.fillStyle = helpers.color(vm.textColor).alpha(vm.opacity).rgbString();
ctx.fillText(label, vm.x + vm.xPadding + vm.fontSize + 3, this.getLineHeight(index + 1));
//A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas)
//ctx.clearRect(vm.x + vm.xPadding, this.getLineHeight(index + 1) - vm.fontSize/2, vm.fontSize, vm.fontSize);
//Instead we'll make a white filled block to put the legendColour palette over.
ctx.fillStyle = helpers.color(vm.legendColors[index].stroke).alpha(vm.opacity).rgbString();
ctx.fillRect(vm.x + vm.xPadding - 1, this.getLineHeight(index + 1) - vm.fontSize / 2 - 1, vm.fontSize + 2, vm.fontSize + 2);
ctx.fillStyle = helpers.color(vm.legendColors[index].fill).alpha(vm.opacity).rgbString();
ctx.fillRect(vm.x + vm.xPadding, this.getLineHeight(index + 1) - vm.fontSize / 2, vm.fontSize, vm.fontSize);
}, this);
break;
}
},
getLineHeight: function(index) {
var baseLineHeight = this._view.y - (this._view.height / 2) + this._view.yPadding,
afterTitleIndex = index - 1;
//If the index is zero, we're getting the title
if (index === 0) {
return baseLineHeight + this._view.titleFontSize / 2;
} else {
return baseLineHeight + ((this._view.fontSize * 1.5 * afterTitleIndex) + this._view.fontSize / 2) + this._view.titleFontSize * 1.5;
}
},
});
}).call(this);