Merge pull request #1829 from nnnick/fix/1716-1731

Better tooltip positioning
This commit is contained in:
Tanner Linsley 2015-12-23 10:22:34 -07:00
commit 31721a353c
4 changed files with 266 additions and 211 deletions

View File

@ -400,6 +400,7 @@
initToolTip: function initToolTip() {
this.tooltip = new Chart.Tooltip({
_chart: this.chart,
_chartInstance: this,
_data: this.data,
_options: this.options,
}, this);

View File

@ -283,9 +283,15 @@
helpers.extend(this._model, {
x: Math.round(tooltipPosition.x),
y: Math.round(tooltipPosition.y),
caretPadding: tooltipPosition.padding,
caretPadding: helpers.getValueOrDefault(tooltipPosition.padding, 2),
labelColors: labelColors,
});
// We need to determine alignment of
var tooltipSize = this.getTooltipSize(this._model);
this.determineAlignment(tooltipSize); // Smart Tooltip placement to stay on the canvas
helpers.extend(this._model, this.getBackgroundPoint(this._model, tooltipSize));
}
else{
this._model.opacity = 0;
@ -297,227 +303,282 @@
return this;
},
draw: function() {
getTooltipSize: function getTooltipSize(vm) {
var ctx = this._chart.ctx;
var size = {
height: vm.yPadding * 2, // Tooltip Padding
width: 0
};
var combinedBodyLength = vm.body.length + vm.beforeBody.length + vm.afterBody.length;
size.height += vm.title.length * vm.titleFontSize; // Title Lines
size.height += (vm.title.length - 1) * vm.titleSpacing; // Title Line Spacing
size.height += vm.title.length ? vm.titleMarginBottom : 0; // Title's bottom Margin
size.height += combinedBodyLength * vm.bodyFontSize; // Body Lines
size.height += combinedBodyLength ? (combinedBodyLength - 1) * vm.bodySpacing : 0; // Body Line Spacing
size.height += vm.footer.length ? vm.footerMarginTop : 0; // Footer Margin
size.height += vm.footer.length * (vm.footerFontSize); // Footer Lines
size.height += vm.footer.length ? (vm.footer.length - 1) * vm.footerSpacing : 0; // Footer Line Spacing
// Width
ctx.font = helpers.fontString(vm.titleFontSize, vm._titleFontStyle, vm._titleFontFamily);
helpers.each(vm.title, function(line) {
size.width = Math.max(size.width, ctx.measureText(line).width);
});
ctx.font = helpers.fontString(vm.bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily);
helpers.each(vm.beforeBody.concat(vm.afterBody), function(line) {
size.width = Math.max(size.width, ctx.measureText(line).width);
}, this);
helpers.each(vm.body, function(line) {
size.width = Math.max(size.width, ctx.measureText(line).width + (this._options.tooltips.mode !== 'single' ? (vm.bodyFontSize + 2) : 0));
}, this);
ctx.font = helpers.fontString(vm.footerFontSize, vm._footerFontStyle, vm._footerFontFamily);
helpers.each(vm.footer, function(line) {
size.width = Math.max(size.width, ctx.measureText(line).width);
});
size.width += 2 * vm.xPadding;
return size;
},
determineAlignment: function determineAlignment(size) {
this._model.xAlign = this._model.yAlign = "center";
if (this._model.y < size.height) {
this._model.yAlign = 'top';
} else if (this._model.y > (this._chart.height - size.height)) {
this._model.yAlign = 'bottom';
}
var lf, rf;
var _this = this;
var midX = (this._chartInstance.chartArea.left + this._chartInstance.chartArea.right) / 2;
if (this._model.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 >= (_this._chart.width - (size.width / 2)); };
}
if (lf(this._model.x)) {
this._model.xAlign = 'left';
} else if (rf(this._model.x)) {
this._model.xAlign = 'right';
}
},
getBackgroundPoint: function getBackgroundPoint(vm, size) {
// Background Position
var pt = {
x: vm.x,
y: vm.y
};
if (vm.xAlign === 'right') {
pt.x -= size.width;
} else if (vm.xAlign === 'center') {
pt.x -= (size.width / 2);
}
if (vm.yAlign === 'top') {
pt.y += vm.caretPadding + vm.caretSize;
} else if (vm.yAlign === 'bottom') {
pt.y -= size.height + vm.caretPadding + vm.caretSize;
} else {
pt.y -= (size.height / 2);
}
if (vm.yAlign == 'center') {
if (vm.xAlign === 'left') {
pt.x += vm.caretPadding + vm.caretSize;
} else if (vm.xAlign === 'right') {
pt.x -= vm.caretPadding + vm.caretSize;
}
} else {
if (vm.xAlign === 'left') {
pt.x -= vm.cornerRadius + vm.caretPadding;
} else if (vm.xAlign === 'right') {
pt.x += vm.cornerRadius + vm.caretPadding;
}
}
return pt;
},
drawCaret: function drawCaret(tooltipPoint, size, opacity, caretPadding) {
var vm = this._view;
var ctx = this._chart.ctx;
var x1, x2, x3;
var y1, y2, y3;
if (vm.yAlign === 'center') {
// Left or right side
if (vm.xAlign === 'left') {
x1 = tooltipPoint.x;
x2 = x1 - vm.caretSize;
x3 = x1;
} else {
x1 = tooltipPoint.x + size.width;
x2 = x1 + vm.caretSize;
x3 = x1;
}
y2 = tooltipPoint.y + (size.height / 2);
y1 = y2 - vm.caretSize;
y3 = y2 + vm.caretSize;
} else {
if (vm.xAlign === 'left') {
x1 = tooltipPoint.x + vm.cornerRadius;
x2 = x1 + vm.caretSize;
x3 = x2 + vm.caretSize;
} else if (vm.xAlign === 'right') {
x1 = tooltipPoint.x + size.width - vm.cornerRadius;
x2 = x1 - vm.caretSize;
x3 = x2 - vm.caretSize;
} else {
x2 = tooltipPoint.x + (size.width / 2);
x1 = x2 - vm.caretSize;
x3 = x2 + vm.caretSize;
}
if (vm.yAlign === 'top') {
y1 = tooltipPoint.y;
y2 = y1 - vm.caretSize;
y3 = y1;
} else {
y1 = tooltipPoint.y + size.height;
y2 = y1 + vm.caretSize;
y3 = y1;
}
}
ctx.fillStyle = helpers.color(vm.backgroundColor).alpha(opacity).rgbString();
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.lineTo(x3, y3);
ctx.closePath();
ctx.fill();
},
drawTitle: function drawTitle(pt, vm, ctx, opacity) {
if (vm.title.length) {
ctx.textAlign = vm._titleAlign;
ctx.textBaseline = "top";
ctx.fillStyle = helpers.color(vm.titleColor).alpha(opacity).rgbString();
ctx.font = helpers.fontString(vm.titleFontSize, vm._titleFontStyle, vm._titleFontFamily);
helpers.each(vm.title, function(title, i) {
ctx.fillText(title, pt.x, pt.y);
pt.y += vm.titleFontSize + vm.titleSpacing; // Line Height and spacing
if (i + 1 === vm.title.length) {
pt.y += vm.titleMarginBottom - vm.titleSpacing; // If Last, add margin, remove spacing
}
}, this);
}
},
drawBody: function drawBody(pt, vm, ctx, opacity) {
ctx.textAlign = vm._bodyAlign;
ctx.textBaseline = "top";
ctx.fillStyle = helpers.color(vm.bodyColor).alpha(opacity).rgbString();
ctx.font = helpers.fontString(vm.bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily);
// Before Body
helpers.each(vm.beforeBody, function(beforeBody) {
ctx.fillText(beforeBody, pt.x, pt.y);
pt.y += vm.bodyFontSize + vm.bodySpacing;
});
helpers.each(vm.body, function(body, i) {
// Draw Legend-like boxes if needed
if (this._options.tooltips.mode !== 'single') {
// Fill a white rect so that colours merge nicely if the opacity is < 1
ctx.fillStyle = helpers.color(vm.legendColorBackground).alpha(opacity).rgbaString();
ctx.fillRect(pt.x, pt.y, vm.bodyFontSize, vm.bodyFontSize);
// Border
ctx.strokeStyle = helpers.color(vm.labelColors[i].borderColor).alpha(opacity).rgbaString();
ctx.strokeRect(pt.x, pt.y, vm.bodyFontSize, vm.bodyFontSize);
// Inner square
ctx.fillStyle = helpers.color(vm.labelColors[i].backgroundColor).alpha(opacity).rgbaString();
ctx.fillRect(pt.x + 1, pt.y + 1, vm.bodyFontSize - 2, vm.bodyFontSize - 2);
ctx.fillStyle = helpers.color(vm.bodyColor).alpha(opacity).rgbaString(); // Return fill style for text
}
// Body Line
ctx.fillText(body, pt.x + (this._options.tooltips.mode !== 'single' ? (vm.bodyFontSize + 2) : 0), pt.y);
pt.y += vm.bodyFontSize + vm.bodySpacing;
}, this);
// After Body
helpers.each(vm.afterBody, function(afterBody) {
ctx.fillText(afterBody, pt.x, pt.y);
pt.y += vm.bodyFontSize;
});
pt.y -= vm.bodySpacing; // Remove last body spacing
},
drawFooter: function drawFooter(pt, vm, ctx, opacity) {
if (vm.footer.length) {
pt.y += vm.footerMarginTop;
ctx.textAlign = vm._footerAlign;
ctx.textBaseline = "top";
ctx.fillStyle = helpers.color(vm.footerColor).alpha(opacity).rgbString();
ctx.font = helpers.fontString(vm.footerFontSize, vm._footerFontStyle, vm._footerFontFamily);
helpers.each(vm.footer, function(footer) {
ctx.fillText(footer, pt.x, pt.y);
pt.y += vm.footerFontSize + vm.footerSpacing;
}, this);
}
},
draw: function draw() {
var ctx = this._chart.ctx;
var vm = this._view;
if (this._view.opacity === 0) {
if (vm.opacity === 0) {
return;
}
// Get Dimensions
vm.position = "top";
var caretPadding = vm.caretPadding || 2;
var combinedBodyLength = vm.body.length + vm.beforeBody.length + vm.afterBody.length;
// Height
var tooltipHeight = vm.yPadding * 2; // Tooltip Padding
tooltipHeight += vm.title.length * vm.titleFontSize; // Title Lines
tooltipHeight += (vm.title.length - 1) * vm.titleSpacing; // Title Line Spacing
tooltipHeight += vm.title.length ? vm.titleMarginBottom : 0; // Title's bottom Margin
tooltipHeight += combinedBodyLength * vm.bodyFontSize; // Body Lines
tooltipHeight += combinedBodyLength ? (combinedBodyLength - 1) * vm.bodySpacing : 0; // Body Line Spacing
tooltipHeight += vm.footer.length ? vm.footerMarginTop : 0; // Footer Margin
tooltipHeight += vm.footer.length * (vm.footerFontSize); // Footer Lines
tooltipHeight += vm.footer.length ? (vm.footer.length - 1) * vm.footerSpacing : 0; // Footer Line Spacing
// Width
var tooltipWidth = 0;
helpers.each(vm.title, function(line) {
ctx.font = helpers.fontString(vm.titleFontSize, vm._titleFontStyle, vm._titleFontFamily);
tooltipWidth = Math.max(tooltipWidth, ctx.measureText(line).width);
});
helpers.each(vm.beforeBody, function(line) {
ctx.font = helpers.fontString(vm.bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily);
tooltipWidth = Math.max(tooltipWidth, ctx.measureText(line).width);
}, this);
helpers.each(vm.body, function(line) {
ctx.font = helpers.fontString(vm.bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily);
tooltipWidth = Math.max(tooltipWidth, ctx.measureText(line).width + (this._options.tooltips.mode !== 'single' ? (vm.bodyFontSize + 2) : 0));
}, this);
helpers.each(vm.afterBody, function(line) {
ctx.font = helpers.fontString(vm.bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily);
tooltipWidth = Math.max(tooltipWidth, ctx.measureText(line).width);
}, this);
helpers.each(vm.footer, function(line) {
ctx.font = helpers.fontString(vm.footerFontSize, vm._footerFontStyle, vm._footerFontFamily);
tooltipWidth = Math.max(tooltipWidth, ctx.measureText(line).width);
});
tooltipWidth += 2 * vm.xPadding;
var tooltipTotalWidth = tooltipWidth + vm.caretSize + caretPadding;
// Smart Tooltip placement to stay on the canvas
// Top, center, or bottom
vm.yAlign = "center";
if (vm.y - (tooltipHeight / 2) < 0) {
vm.yAlign = "top";
} else if (vm.y + (tooltipHeight / 2) > this._chart.height) {
vm.yAlign = "bottom";
}
// Left or Right
vm.xAlign = "right";
if (vm.x + tooltipTotalWidth > this._chart.width) {
vm.xAlign = "left";
}
// Background Position
var tooltipX = vm.x,
tooltipY = vm.y;
if (vm.yAlign === 'top') {
tooltipY = vm.y - vm.caretSize - vm.cornerRadius;
} else if (vm.yAlign === 'bottom') {
tooltipY = vm.y - tooltipHeight + vm.caretSize + vm.cornerRadius;
} else {
tooltipY = vm.y - (tooltipHeight / 2);
}
if (vm.xAlign === 'left') {
tooltipX = vm.x - tooltipTotalWidth;
} else if (vm.xAlign === 'right') {
tooltipX = vm.x + caretPadding + vm.caretSize;
} else {
tooltipX = vm.x + (tooltipTotalWidth / 2);
}
// Draw Background
var caretPadding = vm.caretPadding;
var tooltipSize = this.getTooltipSize(vm);
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;
if (this._options.tooltips.enabled) {
// Draw Background
ctx.fillStyle = helpers.color(vm.backgroundColor).alpha(opacity).rgbString();
helpers.drawRoundedRectangle(ctx, tooltipX, tooltipY, tooltipWidth, tooltipHeight, vm.cornerRadius);
helpers.drawRoundedRectangle(ctx, pt.x, pt.y, tooltipSize.width, tooltipSize.height, vm.cornerRadius);
ctx.fill();
}
// Draw Caret
if (this._options.tooltips.enabled) {
ctx.fillStyle = helpers.color(vm.backgroundColor).alpha(opacity).rgbString();
if (vm.xAlign === 'left') {
ctx.beginPath();
ctx.moveTo(vm.x - caretPadding, vm.y);
ctx.lineTo(vm.x - caretPadding - vm.caretSize, vm.y - vm.caretSize);
ctx.lineTo(vm.x - caretPadding - vm.caretSize, vm.y + vm.caretSize);
ctx.closePath();
ctx.fill();
} else {
ctx.beginPath();
ctx.moveTo(vm.x + caretPadding, vm.y);
ctx.lineTo(vm.x + caretPadding + vm.caretSize, vm.y - vm.caretSize);
ctx.lineTo(vm.x + caretPadding + vm.caretSize, vm.y + vm.caretSize);
ctx.closePath();
ctx.fill();
}
}
// Draw Title, Body, and Footer
if (this._options.tooltips.enabled) {
var yBase = tooltipY + vm.yPadding;
var xBase = tooltipX + vm.xPadding;
// Draw Caret
this.drawCaret(pt, tooltipSize, opacity, caretPadding);
// Draw Title, Body, and Footer
pt.x += vm.xPadding;
pt.y += vm.yPadding;
// Titles
if (vm.title.length) {
ctx.textAlign = vm._titleAlign;
ctx.textBaseline = "top";
ctx.fillStyle = helpers.color(vm.titleColor).alpha(opacity).rgbString();
ctx.font = helpers.fontString(vm.titleFontSize, vm._titleFontStyle, vm._titleFontFamily);
helpers.each(vm.title, function(title, i) {
ctx.fillText(title, xBase, yBase);
yBase += vm.titleFontSize + vm.titleSpacing; // Line Height and spacing
if (i + 1 === vm.title.length) {
yBase += vm.titleMarginBottom - vm.titleSpacing; // If Last, add margin, remove spacing
}
}, this);
}
this.drawTitle(pt, vm, ctx, opacity);
// Body
ctx.textAlign = vm._bodyAlign;
ctx.textBaseline = "top";
ctx.fillStyle = helpers.color(vm.bodyColor).alpha(opacity).rgbString();
ctx.font = helpers.fontString(vm.bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily);
// Before Body
helpers.each(vm.beforeBody, function(beforeBody) {
ctx.fillText(beforeBody, xBase, yBase);
yBase += vm.bodyFontSize + vm.bodySpacing;
});
helpers.each(vm.body, function(body, i) {
// Draw Legend-like boxes if needed
if (this._options.tooltips.mode !== 'single') {
// Fill a white rect so that colours merge nicely if the opacity is < 1
ctx.fillStyle = helpers.color(vm.legendColorBackground).alpha(opacity).rgbaString();
ctx.fillRect(xBase, yBase, vm.bodyFontSize, vm.bodyFontSize);
// Border
ctx.strokeStyle = helpers.color(vm.labelColors[i].borderColor).alpha(opacity).rgbaString();
ctx.strokeRect(xBase, yBase, vm.bodyFontSize, vm.bodyFontSize);
// Inner square
ctx.fillStyle = helpers.color(vm.labelColors[i].backgroundColor).alpha(opacity).rgbaString();
ctx.fillRect(xBase + 1, yBase + 1, vm.bodyFontSize - 2, vm.bodyFontSize - 2);
ctx.fillStyle = helpers.color(vm.bodyColor).alpha(opacity).rgbaString(); // Return fill style for text
}
// Body Line
ctx.fillText(body, xBase + (this._options.tooltips.mode !== 'single' ? (vm.bodyFontSize + 2) : 0), yBase);
yBase += vm.bodyFontSize + vm.bodySpacing;
}, this);
// After Body
helpers.each(vm.afterBody, function(afterBody) {
ctx.fillText(afterBody, xBase, yBase);
yBase += vm.bodyFontSize;
});
yBase -= vm.bodySpacing; // Remove last body spacing
this.drawBody(pt, vm, ctx, opacity);
// Footer
if (vm.footer.length) {
yBase += vm.footerMarginTop;
ctx.textAlign = vm._footerAlign;
ctx.textBaseline = "top";
ctx.fillStyle = helpers.color(vm.footerColor).alpha(opacity).rgbString();
ctx.font = helpers.fontString(vm.footerFontSize, vm._footerFontStyle, vm._footerFontFamily);
helpers.each(vm.footer, function(footer) {
ctx.fillText(footer, xBase, yBase);
yBase += vm.footerFontSize + vm.footerSpacing;
}, this);
}
this.drawFooter(pt, vm, ctx, opacity);
}
},
}
});
}).call(this);

View File

@ -78,17 +78,10 @@
},
tooltipPosition: function() {
var vm = this._view;
if (vm.y < vm.base) {
return {
x: vm.x,
y: vm.y
};
} else {
return {
x: vm.x,
y: vm.base
};
}
return {
x: vm.x,
y: vm.y
};
},
});

View File

@ -109,7 +109,7 @@ describe('Rectangle element tests', function() {
expect(rectangle.tooltipPosition()).toEqual({
x: 10,
y: 0,
y: 15,
});
// Test when the y is below the base (negative bar)