mirror of
https://github.com/chartjs/Chart.js.git
synced 2025-12-08 20:36:08 +00:00
1046 lines
28 KiB
JavaScript
1046 lines
28 KiB
JavaScript
'use strict';
|
|
|
|
const defaults = require('./core.defaults');
|
|
const Element = require('./core.element');
|
|
const helpers = require('../helpers/index');
|
|
|
|
const valueOrDefault = helpers.valueOrDefault;
|
|
const getRtlHelper = helpers.rtl.getRtlAdapter;
|
|
|
|
defaults._set('global', {
|
|
tooltips: {
|
|
enabled: true,
|
|
custom: null,
|
|
mode: 'nearest',
|
|
position: 'average',
|
|
intersect: true,
|
|
backgroundColor: 'rgba(0,0,0,0.8)',
|
|
titleFontStyle: 'bold',
|
|
titleSpacing: 2,
|
|
titleMarginBottom: 6,
|
|
titleFontColor: '#fff',
|
|
titleAlign: 'left',
|
|
bodySpacing: 2,
|
|
bodyFontColor: '#fff',
|
|
bodyAlign: 'left',
|
|
footerFontStyle: 'bold',
|
|
footerSpacing: 2,
|
|
footerMarginTop: 6,
|
|
footerFontColor: '#fff',
|
|
footerAlign: 'left',
|
|
yPadding: 6,
|
|
xPadding: 6,
|
|
caretPadding: 2,
|
|
caretSize: 5,
|
|
cornerRadius: 6,
|
|
multiKeyBackground: '#fff',
|
|
displayColors: true,
|
|
borderColor: 'rgba(0,0,0,0)',
|
|
borderWidth: 0,
|
|
callbacks: {
|
|
// Args are: (tooltipItems, data)
|
|
beforeTitle: helpers.noop,
|
|
title: function(tooltipItems, data) {
|
|
var title = '';
|
|
var labels = data.labels;
|
|
var labelCount = labels ? labels.length : 0;
|
|
|
|
if (tooltipItems.length > 0) {
|
|
var item = tooltipItems[0];
|
|
if (item.label) {
|
|
title = item.label;
|
|
} else if (labelCount > 0 && item.index < labelCount) {
|
|
title = labels[item.index];
|
|
}
|
|
}
|
|
|
|
return title;
|
|
},
|
|
afterTitle: helpers.noop,
|
|
|
|
// Args are: (tooltipItems, data)
|
|
beforeBody: helpers.noop,
|
|
|
|
// Args are: (tooltipItem, data)
|
|
beforeLabel: helpers.noop,
|
|
label: function(tooltipItem, data) {
|
|
var label = data.datasets[tooltipItem.datasetIndex].label || '';
|
|
|
|
if (label) {
|
|
label += ': ';
|
|
}
|
|
if (!helpers.isNullOrUndef(tooltipItem.value)) {
|
|
label += tooltipItem.value;
|
|
}
|
|
return label;
|
|
},
|
|
labelColor: function(tooltipItem, chart) {
|
|
var meta = chart.getDatasetMeta(tooltipItem.datasetIndex);
|
|
var activeElement = meta.data[tooltipItem.index];
|
|
var view = activeElement.$previousStyle || activeElement._view;
|
|
return {
|
|
borderColor: view.borderColor,
|
|
backgroundColor: view.backgroundColor
|
|
};
|
|
},
|
|
labelTextColor: function() {
|
|
return this._options.bodyFontColor;
|
|
},
|
|
afterLabel: helpers.noop,
|
|
|
|
// Args are: (tooltipItems, data)
|
|
afterBody: helpers.noop,
|
|
|
|
// Args are: (tooltipItems, data)
|
|
beforeFooter: helpers.noop,
|
|
footer: helpers.noop,
|
|
afterFooter: helpers.noop
|
|
}
|
|
}
|
|
});
|
|
|
|
var positioners = {
|
|
/**
|
|
* Average mode places the tooltip at the average position of the elements shown
|
|
* @function Chart.Tooltip.positioners.average
|
|
* @param elements {ChartElement[]} the elements being displayed in the tooltip
|
|
* @returns {object} tooltip position
|
|
*/
|
|
average: function(elements) {
|
|
if (!elements.length) {
|
|
return false;
|
|
}
|
|
|
|
var i, len;
|
|
var x = 0;
|
|
var y = 0;
|
|
var count = 0;
|
|
|
|
for (i = 0, len = elements.length; i < len; ++i) {
|
|
var el = elements[i];
|
|
if (el && el.hasValue()) {
|
|
var pos = el.tooltipPosition();
|
|
x += pos.x;
|
|
y += pos.y;
|
|
++count;
|
|
}
|
|
}
|
|
|
|
return {
|
|
x: x / count,
|
|
y: y / count
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Gets the tooltip position nearest of the item nearest to the event position
|
|
* @function Chart.Tooltip.positioners.nearest
|
|
* @param elements {Chart.Element[]} the tooltip elements
|
|
* @param eventPosition {object} the position of the event in canvas coordinates
|
|
* @returns {object} the tooltip position
|
|
*/
|
|
nearest: function(elements, eventPosition) {
|
|
var x = eventPosition.x;
|
|
var y = eventPosition.y;
|
|
var minDistance = Number.POSITIVE_INFINITY;
|
|
var i, len, nearestElement;
|
|
|
|
for (i = 0, len = elements.length; i < len; ++i) {
|
|
var el = elements[i];
|
|
if (el && el.hasValue()) {
|
|
var center = el.getCenterPoint();
|
|
var d = helpers.distanceBetweenPoints(eventPosition, center);
|
|
|
|
if (d < minDistance) {
|
|
minDistance = d;
|
|
nearestElement = el;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (nearestElement) {
|
|
var tp = nearestElement.tooltipPosition();
|
|
x = tp.x;
|
|
y = tp.y;
|
|
}
|
|
|
|
return {
|
|
x: x,
|
|
y: y
|
|
};
|
|
}
|
|
};
|
|
|
|
// Helper to push or concat based on if the 2nd parameter is an array or not
|
|
function pushOrConcat(base, toPush) {
|
|
if (toPush) {
|
|
if (helpers.isArray(toPush)) {
|
|
// base = base.concat(toPush);
|
|
Array.prototype.push.apply(base, toPush);
|
|
} else {
|
|
base.push(toPush);
|
|
}
|
|
}
|
|
|
|
return base;
|
|
}
|
|
|
|
/**
|
|
* Returns array of strings split by newline
|
|
* @param {string} value - The value to split by newline.
|
|
* @returns {string[]} value if newline present - Returned from String split() method
|
|
* @function
|
|
*/
|
|
function splitNewlines(str) {
|
|
if ((typeof str === 'string' || str instanceof String) && str.indexOf('\n') > -1) {
|
|
return str.split('\n');
|
|
}
|
|
return str;
|
|
}
|
|
|
|
|
|
/**
|
|
* Private helper to create a tooltip item model
|
|
* @param element - the chart element (point, arc, bar) to create the tooltip item for
|
|
* @return new tooltip item
|
|
*/
|
|
function createTooltipItem(chart, element) {
|
|
var datasetIndex = element._datasetIndex;
|
|
var index = element._index;
|
|
var controller = chart.getDatasetMeta(datasetIndex).controller;
|
|
var indexScale = controller._getIndexScale();
|
|
var valueScale = controller._getValueScale();
|
|
var parsed = controller._getParsed(index);
|
|
|
|
return {
|
|
label: indexScale ? '' + indexScale.getLabelForValue(parsed[indexScale.id]) : '',
|
|
value: valueScale ? '' + valueScale.getLabelForValue(parsed[valueScale.id]) : '',
|
|
index: index,
|
|
datasetIndex: datasetIndex,
|
|
x: element._model.x,
|
|
y: element._model.y
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Helper to get the reset model for the tooltip
|
|
* @param tooltipOpts {object} the tooltip options
|
|
*/
|
|
function getBaseModel(tooltipOpts) {
|
|
var globalDefaults = defaults.global;
|
|
|
|
return {
|
|
// Positioning
|
|
xPadding: tooltipOpts.xPadding,
|
|
yPadding: tooltipOpts.yPadding,
|
|
xAlign: tooltipOpts.xAlign,
|
|
yAlign: tooltipOpts.yAlign,
|
|
|
|
// Drawing direction and text direction
|
|
rtl: tooltipOpts.rtl,
|
|
textDirection: tooltipOpts.textDirection,
|
|
|
|
// Body
|
|
bodyFontColor: tooltipOpts.bodyFontColor,
|
|
_bodyFontFamily: valueOrDefault(tooltipOpts.bodyFontFamily, globalDefaults.defaultFontFamily),
|
|
_bodyFontStyle: valueOrDefault(tooltipOpts.bodyFontStyle, globalDefaults.defaultFontStyle),
|
|
_bodyAlign: tooltipOpts.bodyAlign,
|
|
bodyFontSize: valueOrDefault(tooltipOpts.bodyFontSize, globalDefaults.defaultFontSize),
|
|
bodySpacing: tooltipOpts.bodySpacing,
|
|
|
|
// Title
|
|
titleFontColor: tooltipOpts.titleFontColor,
|
|
_titleFontFamily: valueOrDefault(tooltipOpts.titleFontFamily, globalDefaults.defaultFontFamily),
|
|
_titleFontStyle: valueOrDefault(tooltipOpts.titleFontStyle, globalDefaults.defaultFontStyle),
|
|
titleFontSize: valueOrDefault(tooltipOpts.titleFontSize, globalDefaults.defaultFontSize),
|
|
_titleAlign: tooltipOpts.titleAlign,
|
|
titleSpacing: tooltipOpts.titleSpacing,
|
|
titleMarginBottom: tooltipOpts.titleMarginBottom,
|
|
|
|
// Footer
|
|
footerFontColor: tooltipOpts.footerFontColor,
|
|
_footerFontFamily: valueOrDefault(tooltipOpts.footerFontFamily, globalDefaults.defaultFontFamily),
|
|
_footerFontStyle: valueOrDefault(tooltipOpts.footerFontStyle, globalDefaults.defaultFontStyle),
|
|
footerFontSize: valueOrDefault(tooltipOpts.footerFontSize, globalDefaults.defaultFontSize),
|
|
_footerAlign: tooltipOpts.footerAlign,
|
|
footerSpacing: tooltipOpts.footerSpacing,
|
|
footerMarginTop: tooltipOpts.footerMarginTop,
|
|
|
|
// Appearance
|
|
caretSize: tooltipOpts.caretSize,
|
|
cornerRadius: tooltipOpts.cornerRadius,
|
|
backgroundColor: tooltipOpts.backgroundColor,
|
|
opacity: 0,
|
|
legendColorBackground: tooltipOpts.multiKeyBackground,
|
|
displayColors: tooltipOpts.displayColors,
|
|
borderColor: tooltipOpts.borderColor,
|
|
borderWidth: tooltipOpts.borderWidth
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get the size of the tooltip
|
|
*/
|
|
function getTooltipSize(tooltip, model) {
|
|
var ctx = tooltip._chart.ctx;
|
|
|
|
var height = model.yPadding * 2; // Tooltip Padding
|
|
var width = 0;
|
|
|
|
// Count of all lines in the body
|
|
var body = model.body;
|
|
var combinedBodyLength = body.reduce(function(count, bodyItem) {
|
|
return count + bodyItem.before.length + bodyItem.lines.length + bodyItem.after.length;
|
|
}, 0);
|
|
combinedBodyLength += model.beforeBody.length + model.afterBody.length;
|
|
|
|
var titleLineCount = model.title.length;
|
|
var footerLineCount = model.footer.length;
|
|
var titleFontSize = model.titleFontSize;
|
|
var bodyFontSize = model.bodyFontSize;
|
|
var footerFontSize = model.footerFontSize;
|
|
|
|
height += titleLineCount * titleFontSize; // Title Lines
|
|
height += titleLineCount ? (titleLineCount - 1) * model.titleSpacing : 0; // Title Line Spacing
|
|
height += titleLineCount ? model.titleMarginBottom : 0; // Title's bottom Margin
|
|
height += combinedBodyLength * bodyFontSize; // Body Lines
|
|
height += combinedBodyLength ? (combinedBodyLength - 1) * model.bodySpacing : 0; // Body Line Spacing
|
|
height += footerLineCount ? model.footerMarginTop : 0; // Footer Margin
|
|
height += footerLineCount * (footerFontSize); // Footer Lines
|
|
height += footerLineCount ? (footerLineCount - 1) * model.footerSpacing : 0; // Footer Line Spacing
|
|
|
|
// Title width
|
|
var widthPadding = 0;
|
|
var maxLineWidth = function(line) {
|
|
width = Math.max(width, ctx.measureText(line).width + widthPadding);
|
|
};
|
|
|
|
ctx.font = helpers.fontString(titleFontSize, model._titleFontStyle, model._titleFontFamily);
|
|
helpers.each(model.title, maxLineWidth);
|
|
|
|
// Body width
|
|
ctx.font = helpers.fontString(bodyFontSize, model._bodyFontStyle, model._bodyFontFamily);
|
|
helpers.each(model.beforeBody.concat(model.afterBody), maxLineWidth);
|
|
|
|
// Body lines may include some extra width due to the color box
|
|
widthPadding = model.displayColors ? (bodyFontSize + 2) : 0;
|
|
helpers.each(body, function(bodyItem) {
|
|
helpers.each(bodyItem.before, maxLineWidth);
|
|
helpers.each(bodyItem.lines, maxLineWidth);
|
|
helpers.each(bodyItem.after, maxLineWidth);
|
|
});
|
|
|
|
// Reset back to 0
|
|
widthPadding = 0;
|
|
|
|
// Footer width
|
|
ctx.font = helpers.fontString(footerFontSize, model._footerFontStyle, model._footerFontFamily);
|
|
helpers.each(model.footer, maxLineWidth);
|
|
|
|
// Add padding
|
|
width += 2 * model.xPadding;
|
|
|
|
return {
|
|
width: width,
|
|
height: height
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Helper to get the alignment of a tooltip given the size
|
|
*/
|
|
function determineAlignment(tooltip, size) {
|
|
var model = tooltip._model;
|
|
var chart = tooltip._chart;
|
|
var chartArea = chart.chartArea;
|
|
var xAlign = 'center';
|
|
var yAlign = 'center';
|
|
|
|
if (model.y < size.height) {
|
|
yAlign = 'top';
|
|
} else if (model.y > (chart.height - size.height)) {
|
|
yAlign = 'bottom';
|
|
}
|
|
|
|
var lf, rf; // functions to determine left, right alignment
|
|
var olf, orf; // functions to determine if left/right alignment causes tooltip to go outside chart
|
|
var yf; // function to get the y alignment if the tooltip goes outside of the left or right edges
|
|
var midX = (chartArea.left + chartArea.right) / 2;
|
|
var midY = (chartArea.top + chartArea.bottom) / 2;
|
|
|
|
if (yAlign === 'center') {
|
|
lf = function(x) {
|
|
return x <= midX;
|
|
};
|
|
rf = function(x) {
|
|
return x > midX;
|
|
};
|
|
} else {
|
|
lf = function(x) {
|
|
return x <= (size.width / 2);
|
|
};
|
|
rf = function(x) {
|
|
return x >= (chart.width - (size.width / 2));
|
|
};
|
|
}
|
|
|
|
olf = function(x) {
|
|
return x + size.width + model.caretSize + model.caretPadding > chart.width;
|
|
};
|
|
orf = function(x) {
|
|
return x - size.width - model.caretSize - model.caretPadding < 0;
|
|
};
|
|
yf = function(y) {
|
|
return y <= midY ? 'top' : 'bottom';
|
|
};
|
|
|
|
if (lf(model.x)) {
|
|
xAlign = 'left';
|
|
|
|
// Is tooltip too wide and goes over the right side of the chart.?
|
|
if (olf(model.x)) {
|
|
xAlign = 'center';
|
|
yAlign = yf(model.y);
|
|
}
|
|
} else if (rf(model.x)) {
|
|
xAlign = 'right';
|
|
|
|
// Is tooltip too wide and goes outside left edge of canvas?
|
|
if (orf(model.x)) {
|
|
xAlign = 'center';
|
|
yAlign = yf(model.y);
|
|
}
|
|
}
|
|
|
|
var opts = tooltip._options;
|
|
return {
|
|
xAlign: opts.xAlign ? opts.xAlign : xAlign,
|
|
yAlign: opts.yAlign ? opts.yAlign : yAlign
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Helper to get the location a tooltip needs to be placed at given the initial position (via the vm) and the size and alignment
|
|
*/
|
|
function getBackgroundPoint(vm, size, alignment, chart) {
|
|
// Background Position
|
|
var x = vm.x;
|
|
var y = vm.y;
|
|
|
|
var caretSize = vm.caretSize;
|
|
var caretPadding = vm.caretPadding;
|
|
var cornerRadius = vm.cornerRadius;
|
|
var xAlign = alignment.xAlign;
|
|
var yAlign = alignment.yAlign;
|
|
var paddingAndSize = caretSize + caretPadding;
|
|
var radiusAndPadding = cornerRadius + caretPadding;
|
|
|
|
if (xAlign === 'right') {
|
|
x -= size.width;
|
|
} else if (xAlign === 'center') {
|
|
x -= (size.width / 2);
|
|
if (x + size.width > chart.width) {
|
|
x = chart.width - size.width;
|
|
}
|
|
if (x < 0) {
|
|
x = 0;
|
|
}
|
|
}
|
|
|
|
if (yAlign === 'top') {
|
|
y += paddingAndSize;
|
|
} else if (yAlign === 'bottom') {
|
|
y -= size.height + paddingAndSize;
|
|
} else {
|
|
y -= (size.height / 2);
|
|
}
|
|
|
|
if (yAlign === 'center') {
|
|
if (xAlign === 'left') {
|
|
x += paddingAndSize;
|
|
} else if (xAlign === 'right') {
|
|
x -= paddingAndSize;
|
|
}
|
|
} else if (xAlign === 'left') {
|
|
x -= radiusAndPadding;
|
|
} else if (xAlign === 'right') {
|
|
x += radiusAndPadding;
|
|
}
|
|
|
|
return {
|
|
x: x,
|
|
y: y
|
|
};
|
|
}
|
|
|
|
function getAlignedX(vm, align) {
|
|
return align === 'center'
|
|
? vm.x + vm.width / 2
|
|
: align === 'right'
|
|
? vm.x + vm.width - vm.xPadding
|
|
: vm.x + vm.xPadding;
|
|
}
|
|
|
|
/**
|
|
* Helper to build before and after body lines
|
|
*/
|
|
function getBeforeAfterBodyLines(callback) {
|
|
return pushOrConcat([], splitNewlines(callback));
|
|
}
|
|
|
|
class Tooltip extends Element {
|
|
initialize() {
|
|
var me = this;
|
|
me._model = getBaseModel(me._options);
|
|
me._view = {};
|
|
me._lastActive = [];
|
|
}
|
|
|
|
transition(easingValue) {
|
|
var me = this;
|
|
var options = me._options;
|
|
|
|
if (me._lastEvent && me._chart.animating) {
|
|
// Let's react to changes during animation
|
|
me._active = me._chart.getElementsAtEventForMode(me._lastEvent, options.mode, options);
|
|
me.update(true);
|
|
me.pivot();
|
|
me._lastActive = me.active;
|
|
}
|
|
|
|
Element.prototype.transition.call(me, easingValue);
|
|
}
|
|
|
|
// Get the title
|
|
// Args are: (tooltipItem, data)
|
|
getTitle() {
|
|
var me = this;
|
|
var opts = me._options;
|
|
var callbacks = opts.callbacks;
|
|
|
|
var beforeTitle = callbacks.beforeTitle.apply(me, arguments);
|
|
var title = callbacks.title.apply(me, arguments);
|
|
var afterTitle = callbacks.afterTitle.apply(me, arguments);
|
|
|
|
var lines = [];
|
|
lines = pushOrConcat(lines, splitNewlines(beforeTitle));
|
|
lines = pushOrConcat(lines, splitNewlines(title));
|
|
lines = pushOrConcat(lines, splitNewlines(afterTitle));
|
|
|
|
return lines;
|
|
}
|
|
|
|
// Args are: (tooltipItem, data)
|
|
getBeforeBody() {
|
|
return getBeforeAfterBodyLines(this._options.callbacks.beforeBody.apply(this, arguments));
|
|
}
|
|
|
|
// Args are: (tooltipItem, data)
|
|
getBody(tooltipItems, data) {
|
|
var me = this;
|
|
var callbacks = me._options.callbacks;
|
|
var bodyItems = [];
|
|
|
|
helpers.each(tooltipItems, function(tooltipItem) {
|
|
var bodyItem = {
|
|
before: [],
|
|
lines: [],
|
|
after: []
|
|
};
|
|
pushOrConcat(bodyItem.before, splitNewlines(callbacks.beforeLabel.call(me, tooltipItem, data)));
|
|
pushOrConcat(bodyItem.lines, callbacks.label.call(me, tooltipItem, data));
|
|
pushOrConcat(bodyItem.after, splitNewlines(callbacks.afterLabel.call(me, tooltipItem, data)));
|
|
|
|
bodyItems.push(bodyItem);
|
|
});
|
|
|
|
return bodyItems;
|
|
}
|
|
|
|
// Args are: (tooltipItem, data)
|
|
getAfterBody() {
|
|
return getBeforeAfterBodyLines(this._options.callbacks.afterBody.apply(this, arguments));
|
|
}
|
|
|
|
// Get the footer and beforeFooter and afterFooter lines
|
|
// Args are: (tooltipItem, data)
|
|
getFooter() {
|
|
var me = this;
|
|
var callbacks = me._options.callbacks;
|
|
|
|
var beforeFooter = callbacks.beforeFooter.apply(me, arguments);
|
|
var footer = callbacks.footer.apply(me, arguments);
|
|
var afterFooter = callbacks.afterFooter.apply(me, arguments);
|
|
|
|
var lines = [];
|
|
lines = pushOrConcat(lines, splitNewlines(beforeFooter));
|
|
lines = pushOrConcat(lines, splitNewlines(footer));
|
|
lines = pushOrConcat(lines, splitNewlines(afterFooter));
|
|
|
|
return lines;
|
|
}
|
|
|
|
update(changed) {
|
|
var me = this;
|
|
var opts = me._options;
|
|
|
|
// Need to regenerate the model because its faster than using extend and it is necessary due to the optimization in Chart.Element.transition
|
|
// that does _view = _model if ease === 1. This causes the 2nd tooltip update to set properties in both the view and model at the same time
|
|
// which breaks any animations.
|
|
var existingModel = me._model;
|
|
var model = me._model = getBaseModel(opts);
|
|
var active = me._active;
|
|
|
|
var data = me._data;
|
|
|
|
// In the case where active.length === 0 we need to keep these at existing values for good animations
|
|
var alignment = {
|
|
xAlign: existingModel.xAlign,
|
|
yAlign: existingModel.yAlign
|
|
};
|
|
var backgroundPoint = {
|
|
x: existingModel.x,
|
|
y: existingModel.y
|
|
};
|
|
var tooltipSize = {
|
|
width: existingModel.width,
|
|
height: existingModel.height
|
|
};
|
|
var tooltipPosition = {
|
|
x: existingModel.caretX,
|
|
y: existingModel.caretY
|
|
};
|
|
|
|
var i, len;
|
|
|
|
if (active.length) {
|
|
model.opacity = 1;
|
|
|
|
var labelColors = [];
|
|
var labelTextColors = [];
|
|
tooltipPosition = positioners[opts.position].call(me, active, me._eventPosition);
|
|
|
|
var tooltipItems = [];
|
|
for (i = 0, len = active.length; i < len; ++i) {
|
|
tooltipItems.push(createTooltipItem(me._chart, active[i]));
|
|
}
|
|
|
|
// If the user provided a filter function, use it to modify the tooltip items
|
|
if (opts.filter) {
|
|
tooltipItems = tooltipItems.filter(function(a) {
|
|
return opts.filter(a, data);
|
|
});
|
|
}
|
|
|
|
// If the user provided a sorting function, use it to modify the tooltip items
|
|
if (opts.itemSort) {
|
|
tooltipItems = tooltipItems.sort(function(a, b) {
|
|
return opts.itemSort(a, b, data);
|
|
});
|
|
}
|
|
|
|
// Determine colors for boxes
|
|
helpers.each(tooltipItems, function(tooltipItem) {
|
|
labelColors.push(opts.callbacks.labelColor.call(me, tooltipItem, me._chart));
|
|
labelTextColors.push(opts.callbacks.labelTextColor.call(me, tooltipItem, me._chart));
|
|
});
|
|
|
|
|
|
// Build the Text Lines
|
|
model.title = me.getTitle(tooltipItems, data);
|
|
model.beforeBody = me.getBeforeBody(tooltipItems, data);
|
|
model.body = me.getBody(tooltipItems, data);
|
|
model.afterBody = me.getAfterBody(tooltipItems, data);
|
|
model.footer = me.getFooter(tooltipItems, data);
|
|
|
|
// Initial positioning and colors
|
|
model.x = tooltipPosition.x;
|
|
model.y = tooltipPosition.y;
|
|
model.caretPadding = opts.caretPadding;
|
|
model.labelColors = labelColors;
|
|
model.labelTextColors = labelTextColors;
|
|
|
|
// data points
|
|
model.dataPoints = tooltipItems;
|
|
|
|
// We need to determine alignment of the tooltip
|
|
tooltipSize = getTooltipSize(this, model);
|
|
alignment = determineAlignment(this, tooltipSize);
|
|
// Final Size and Position
|
|
backgroundPoint = getBackgroundPoint(model, tooltipSize, alignment, me._chart);
|
|
} else {
|
|
model.opacity = 0;
|
|
}
|
|
|
|
model.xAlign = alignment.xAlign;
|
|
model.yAlign = alignment.yAlign;
|
|
model.x = backgroundPoint.x;
|
|
model.y = backgroundPoint.y;
|
|
model.width = tooltipSize.width;
|
|
model.height = tooltipSize.height;
|
|
|
|
// Point where the caret on the tooltip points to
|
|
model.caretX = tooltipPosition.x;
|
|
model.caretY = tooltipPosition.y;
|
|
|
|
me._model = model;
|
|
|
|
if (changed && opts.custom) {
|
|
opts.custom.call(me, model);
|
|
}
|
|
|
|
return me;
|
|
}
|
|
|
|
drawCaret(tooltipPoint, size) {
|
|
var ctx = this._chart.ctx;
|
|
var vm = this._view;
|
|
var caretPosition = this.getCaretPosition(tooltipPoint, size, vm);
|
|
|
|
ctx.lineTo(caretPosition.x1, caretPosition.y1);
|
|
ctx.lineTo(caretPosition.x2, caretPosition.y2);
|
|
ctx.lineTo(caretPosition.x3, caretPosition.y3);
|
|
}
|
|
|
|
getCaretPosition(tooltipPoint, size, vm) {
|
|
var x1, x2, x3, y1, y2, y3;
|
|
var caretSize = vm.caretSize;
|
|
var cornerRadius = vm.cornerRadius;
|
|
var xAlign = vm.xAlign;
|
|
var yAlign = vm.yAlign;
|
|
var ptX = tooltipPoint.x;
|
|
var ptY = tooltipPoint.y;
|
|
var width = size.width;
|
|
var height = size.height;
|
|
|
|
if (yAlign === 'center') {
|
|
y2 = ptY + (height / 2);
|
|
|
|
if (xAlign === 'left') {
|
|
x1 = ptX;
|
|
x2 = x1 - caretSize;
|
|
x3 = x1;
|
|
|
|
y1 = y2 + caretSize;
|
|
y3 = y2 - caretSize;
|
|
} else {
|
|
x1 = ptX + width;
|
|
x2 = x1 + caretSize;
|
|
x3 = x1;
|
|
|
|
y1 = y2 - caretSize;
|
|
y3 = y2 + caretSize;
|
|
}
|
|
} else {
|
|
if (xAlign === 'left') {
|
|
x2 = ptX + cornerRadius + (caretSize);
|
|
x1 = x2 - caretSize;
|
|
x3 = x2 + caretSize;
|
|
} else if (xAlign === 'right') {
|
|
x2 = ptX + width - cornerRadius - caretSize;
|
|
x1 = x2 - caretSize;
|
|
x3 = x2 + caretSize;
|
|
} else {
|
|
x2 = vm.caretX;
|
|
x1 = x2 - caretSize;
|
|
x3 = x2 + caretSize;
|
|
}
|
|
if (yAlign === 'top') {
|
|
y1 = ptY;
|
|
y2 = y1 - caretSize;
|
|
y3 = y1;
|
|
} else {
|
|
y1 = ptY + height;
|
|
y2 = y1 + caretSize;
|
|
y3 = y1;
|
|
// invert drawing order
|
|
var tmp = x3;
|
|
x3 = x1;
|
|
x1 = tmp;
|
|
}
|
|
}
|
|
return {x1: x1, x2: x2, x3: x3, y1: y1, y2: y2, y3: y3};
|
|
}
|
|
|
|
drawTitle(pt, vm, ctx) {
|
|
var title = vm.title;
|
|
var length = title.length;
|
|
var titleFontSize, titleSpacing, i;
|
|
|
|
if (length) {
|
|
var rtlHelper = getRtlHelper(vm.rtl, vm.x, vm.width);
|
|
|
|
pt.x = getAlignedX(vm, vm._titleAlign);
|
|
|
|
ctx.textAlign = rtlHelper.textAlign(vm._titleAlign);
|
|
ctx.textBaseline = 'middle';
|
|
|
|
titleFontSize = vm.titleFontSize;
|
|
titleSpacing = vm.titleSpacing;
|
|
|
|
ctx.fillStyle = vm.titleFontColor;
|
|
ctx.font = helpers.fontString(titleFontSize, vm._titleFontStyle, vm._titleFontFamily);
|
|
|
|
for (i = 0; i < length; ++i) {
|
|
ctx.fillText(title[i], rtlHelper.x(pt.x), pt.y + titleFontSize / 2);
|
|
pt.y += titleFontSize + titleSpacing; // Line Height and spacing
|
|
|
|
if (i + 1 === length) {
|
|
pt.y += vm.titleMarginBottom - titleSpacing; // If Last, add margin, remove spacing
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
drawBody(pt, vm, ctx) {
|
|
var bodyFontSize = vm.bodyFontSize;
|
|
var bodySpacing = vm.bodySpacing;
|
|
var bodyAlign = vm._bodyAlign;
|
|
var body = vm.body;
|
|
var drawColorBoxes = vm.displayColors;
|
|
var xLinePadding = 0;
|
|
var colorX = drawColorBoxes ? getAlignedX(vm, 'left') : 0;
|
|
|
|
var rtlHelper = getRtlHelper(vm.rtl, vm.x, vm.width);
|
|
|
|
var fillLineOfText = function(line) {
|
|
ctx.fillText(line, rtlHelper.x(pt.x + xLinePadding), pt.y + bodyFontSize / 2);
|
|
pt.y += bodyFontSize + bodySpacing;
|
|
};
|
|
|
|
var bodyItem, textColor, labelColors, lines, i, j, ilen, jlen;
|
|
var bodyAlignForCalculation = rtlHelper.textAlign(bodyAlign);
|
|
|
|
ctx.textAlign = bodyAlign;
|
|
ctx.textBaseline = 'middle';
|
|
ctx.font = helpers.fontString(bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily);
|
|
|
|
pt.x = getAlignedX(vm, bodyAlignForCalculation);
|
|
|
|
// Before body lines
|
|
ctx.fillStyle = vm.bodyFontColor;
|
|
helpers.each(vm.beforeBody, fillLineOfText);
|
|
|
|
xLinePadding = drawColorBoxes && bodyAlignForCalculation !== 'right'
|
|
? bodyAlign === 'center' ? (bodyFontSize / 2 + 1) : (bodyFontSize + 2)
|
|
: 0;
|
|
|
|
// Draw body lines now
|
|
for (i = 0, ilen = body.length; i < ilen; ++i) {
|
|
bodyItem = body[i];
|
|
textColor = vm.labelTextColors[i];
|
|
labelColors = vm.labelColors[i];
|
|
|
|
ctx.fillStyle = textColor;
|
|
helpers.each(bodyItem.before, fillLineOfText);
|
|
|
|
lines = bodyItem.lines;
|
|
for (j = 0, jlen = lines.length; j < jlen; ++j) {
|
|
// Draw Legend-like boxes if needed
|
|
if (drawColorBoxes) {
|
|
var rtlColorX = rtlHelper.x(colorX);
|
|
|
|
// Fill a white rect so that colours merge nicely if the opacity is < 1
|
|
ctx.fillStyle = vm.legendColorBackground;
|
|
ctx.fillRect(rtlHelper.leftForLtr(rtlColorX, bodyFontSize), pt.y, bodyFontSize, bodyFontSize);
|
|
|
|
// Border
|
|
ctx.lineWidth = 1;
|
|
ctx.strokeStyle = labelColors.borderColor;
|
|
ctx.strokeRect(rtlHelper.leftForLtr(rtlColorX, bodyFontSize), pt.y, bodyFontSize, bodyFontSize);
|
|
|
|
// Inner square
|
|
ctx.fillStyle = labelColors.backgroundColor;
|
|
ctx.fillRect(rtlHelper.leftForLtr(rtlHelper.xPlus(rtlColorX, 1), bodyFontSize - 2), pt.y + 1, bodyFontSize - 2, bodyFontSize - 2);
|
|
ctx.fillStyle = textColor;
|
|
}
|
|
|
|
fillLineOfText(lines[j]);
|
|
}
|
|
|
|
helpers.each(bodyItem.after, fillLineOfText);
|
|
}
|
|
|
|
// Reset back to 0 for after body
|
|
xLinePadding = 0;
|
|
|
|
// After body lines
|
|
helpers.each(vm.afterBody, fillLineOfText);
|
|
pt.y -= bodySpacing; // Remove last body spacing
|
|
}
|
|
|
|
drawFooter(pt, vm, ctx) {
|
|
var footer = vm.footer;
|
|
var length = footer.length;
|
|
var footerFontSize, i;
|
|
|
|
if (length) {
|
|
var rtlHelper = getRtlHelper(vm.rtl, vm.x, vm.width);
|
|
|
|
pt.x = getAlignedX(vm, vm._footerAlign);
|
|
pt.y += vm.footerMarginTop;
|
|
|
|
ctx.textAlign = rtlHelper.textAlign(vm._footerAlign);
|
|
ctx.textBaseline = 'middle';
|
|
|
|
footerFontSize = vm.footerFontSize;
|
|
|
|
ctx.fillStyle = vm.footerFontColor;
|
|
ctx.font = helpers.fontString(footerFontSize, vm._footerFontStyle, vm._footerFontFamily);
|
|
|
|
for (i = 0; i < length; ++i) {
|
|
ctx.fillText(footer[i], rtlHelper.x(pt.x), pt.y + footerFontSize / 2);
|
|
pt.y += footerFontSize + vm.footerSpacing;
|
|
}
|
|
}
|
|
}
|
|
|
|
drawBackground(pt, vm, ctx, tooltipSize) {
|
|
ctx.fillStyle = vm.backgroundColor;
|
|
ctx.strokeStyle = vm.borderColor;
|
|
ctx.lineWidth = vm.borderWidth;
|
|
var xAlign = vm.xAlign;
|
|
var yAlign = vm.yAlign;
|
|
var x = pt.x;
|
|
var y = pt.y;
|
|
var width = tooltipSize.width;
|
|
var height = tooltipSize.height;
|
|
var radius = vm.cornerRadius;
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(x + radius, y);
|
|
if (yAlign === 'top') {
|
|
this.drawCaret(pt, tooltipSize);
|
|
}
|
|
ctx.lineTo(x + width - radius, y);
|
|
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
if (yAlign === 'center' && xAlign === 'right') {
|
|
this.drawCaret(pt, tooltipSize);
|
|
}
|
|
ctx.lineTo(x + width, y + height - radius);
|
|
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
if (yAlign === 'bottom') {
|
|
this.drawCaret(pt, tooltipSize);
|
|
}
|
|
ctx.lineTo(x + radius, y + height);
|
|
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
if (yAlign === 'center' && xAlign === 'left') {
|
|
this.drawCaret(pt, tooltipSize);
|
|
}
|
|
ctx.lineTo(x, y + radius);
|
|
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
ctx.closePath();
|
|
|
|
ctx.fill();
|
|
|
|
if (vm.borderWidth > 0) {
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
draw() {
|
|
var ctx = this._chart.ctx;
|
|
var vm = this._view;
|
|
|
|
if (vm.opacity === 0) {
|
|
return;
|
|
}
|
|
|
|
var tooltipSize = {
|
|
width: vm.width,
|
|
height: vm.height
|
|
};
|
|
var pt = {
|
|
x: vm.x,
|
|
y: vm.y
|
|
};
|
|
|
|
// IE11/Edge does not like very small opacities, so snap to 0
|
|
var opacity = Math.abs(vm.opacity < 1e-3) ? 0 : vm.opacity;
|
|
|
|
// Truthy/falsey value for empty tooltip
|
|
var hasTooltipContent = vm.title.length || vm.beforeBody.length || vm.body.length || vm.afterBody.length || vm.footer.length;
|
|
|
|
if (this._options.enabled && hasTooltipContent) {
|
|
ctx.save();
|
|
ctx.globalAlpha = opacity;
|
|
|
|
// Draw Background
|
|
this.drawBackground(pt, vm, ctx, tooltipSize);
|
|
|
|
// Draw Title, Body, and Footer
|
|
pt.y += vm.yPadding;
|
|
|
|
helpers.rtl.overrideTextDirection(ctx, vm.textDirection);
|
|
|
|
// Titles
|
|
this.drawTitle(pt, vm, ctx);
|
|
|
|
// Body
|
|
this.drawBody(pt, vm, ctx);
|
|
|
|
// Footer
|
|
this.drawFooter(pt, vm, ctx);
|
|
|
|
helpers.rtl.restoreTextDirection(ctx, vm.textDirection);
|
|
|
|
ctx.restore();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle an event
|
|
* @private
|
|
* @param {IEvent} event - The event to handle
|
|
* @returns {boolean} true if the tooltip changed
|
|
*/
|
|
handleEvent(e) {
|
|
var me = this;
|
|
var options = me._options;
|
|
var changed = false;
|
|
|
|
me._lastActive = me._lastActive || [];
|
|
|
|
// Find Active Elements for tooltips
|
|
if (e.type === 'mouseout') {
|
|
me._active = [];
|
|
me._lastEvent = null;
|
|
} else {
|
|
me._active = me._chart.getElementsAtEventForMode(e, options.mode, options);
|
|
if (e.type !== 'click') {
|
|
me._lastEvent = e.type === 'click' ? null : e;
|
|
}
|
|
if (options.reverse) {
|
|
me._active.reverse();
|
|
}
|
|
}
|
|
|
|
// Remember Last Actives
|
|
changed = !helpers.arrayEquals(me._active, me._lastActive);
|
|
|
|
// Only handle target event on tooltip change
|
|
if (changed) {
|
|
me._lastActive = me._active;
|
|
|
|
if (options.enabled || options.custom) {
|
|
me._eventPosition = {
|
|
x: e.x,
|
|
y: e.y
|
|
};
|
|
|
|
me.update(true);
|
|
me.pivot();
|
|
}
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @namespace Chart.Tooltip.positioners
|
|
*/
|
|
Tooltip.positioners = positioners;
|
|
|
|
module.exports = Tooltip;
|