diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index fb081e60b31..127720a415e 100644 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -463,6 +463,14 @@ function getBackgroundPoint(vm, size, alignment, chart) { }; } +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 */ @@ -730,6 +738,8 @@ var exports = Element.extend({ var title = vm.title; if (title.length) { + pt.x = getAlignedX(vm, vm._titleAlign); + ctx.textAlign = vm._titleAlign; ctx.textBaseline = 'top'; @@ -754,14 +764,21 @@ var exports = Element.extend({ drawBody: function(pt, vm, ctx) { var bodyFontSize = vm.bodyFontSize; var bodySpacing = vm.bodySpacing; + var bodyAlign = vm._bodyAlign; var body = vm.body; + var drawColorBoxes = vm.displayColors; + var labelColors = vm.labelColors; + var xLinePadding = 0; + var colorX = drawColorBoxes ? getAlignedX(vm, 'left') : 0; + var textColor; - ctx.textAlign = vm._bodyAlign; + ctx.textAlign = bodyAlign; ctx.textBaseline = 'top'; ctx.font = helpers.fontString(bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily); + pt.x = getAlignedX(vm, bodyAlign); + // Before Body - var xLinePadding = 0; var fillLineOfText = function(line) { ctx.fillText(line, pt.x + xLinePadding, pt.y); pt.y += bodyFontSize + bodySpacing; @@ -771,12 +788,13 @@ var exports = Element.extend({ ctx.fillStyle = vm.bodyFontColor; helpers.each(vm.beforeBody, fillLineOfText); - var drawColorBoxes = vm.displayColors; - xLinePadding = drawColorBoxes ? (bodyFontSize + 2) : 0; + xLinePadding = drawColorBoxes && bodyAlign !== 'right' + ? bodyAlign === 'center' ? (bodyFontSize / 2 + 1) : (bodyFontSize + 2) + : 0; // Draw body lines now helpers.each(body, function(bodyItem, i) { - var textColor = vm.labelTextColors[i]; + textColor = vm.labelTextColors[i]; ctx.fillStyle = textColor; helpers.each(bodyItem.before, fillLineOfText); @@ -785,16 +803,16 @@ var exports = Element.extend({ if (drawColorBoxes) { // Fill a white rect so that colours merge nicely if the opacity is < 1 ctx.fillStyle = vm.legendColorBackground; - ctx.fillRect(pt.x, pt.y, bodyFontSize, bodyFontSize); + ctx.fillRect(colorX, pt.y, bodyFontSize, bodyFontSize); // Border ctx.lineWidth = 1; - ctx.strokeStyle = vm.labelColors[i].borderColor; - ctx.strokeRect(pt.x, pt.y, bodyFontSize, bodyFontSize); + ctx.strokeStyle = labelColors[i].borderColor; + ctx.strokeRect(colorX, pt.y, bodyFontSize, bodyFontSize); // Inner square - ctx.fillStyle = vm.labelColors[i].backgroundColor; - ctx.fillRect(pt.x + 1, pt.y + 1, bodyFontSize - 2, bodyFontSize - 2); + ctx.fillStyle = labelColors[i].backgroundColor; + ctx.fillRect(colorX + 1, pt.y + 1, bodyFontSize - 2, bodyFontSize - 2); ctx.fillStyle = textColor; } @@ -816,6 +834,7 @@ var exports = Element.extend({ var footer = vm.footer; if (footer.length) { + pt.x = getAlignedX(vm, vm._footerAlign); pt.y += vm.footerMarginTop; ctx.textAlign = vm._footerAlign; @@ -905,7 +924,6 @@ var exports = Element.extend({ this.drawBackground(pt, vm, ctx, tooltipSize); // Draw Title, Body, and Footer - pt.x += vm.xPadding; pt.y += vm.yPadding; // Titles diff --git a/test/specs/core.tooltip.tests.js b/test/specs/core.tooltip.tests.js index b9b93d0116a..a994b4ac214 100755 --- a/test/specs/core.tooltip.tests.js +++ b/test/specs/core.tooltip.tests.js @@ -1099,4 +1099,174 @@ describe('Core.Tooltip', function() { }] })); }); + + describe('text align', function() { + var globalDefaults = Chart.defaults.global; + var makeView = function(title, body, footer) { + return { + // Positioning + x: 100, + y: 100, + width: 100, + height: 100, + xPadding: 5, + yPadding: 5, + xAlign: 'left', + yAlign: 'top', + + // Body + bodyFontColor: '#fff', + _bodyFontFamily: globalDefaults.defaultFontFamily, + _bodyFontStyle: globalDefaults.defaultFontStyle, + _bodyAlign: body, + bodyFontSize: globalDefaults.defaultFontSize, + bodySpacing: 2, + + // Title + titleFontColor: '#fff', + _titleFontFamily: globalDefaults.defaultFontFamily, + _titleFontStyle: 'bold', + titleFontSize: globalDefaults.defaultFontSize, + _titleAlign: title, + titleSpacing: 2, + titleMarginBottom: 6, + + // Footer + footerFontColor: '#fff', + _footerFontFamily: globalDefaults.defaultFontFamily, + _footerFontStyle: 'bold', + footerFontSize: globalDefaults.defaultFontSize, + _footerAlign: footer, + footerSpacing: 2, + footerMarginTop: 6, + + // Appearance + caretSize: 5, + cornerRadius: 6, + borderColor: '#aaa', + borderWidth: 1, + backgroundColor: 'rgba(0,0,0,0.8)', + opacity: 1, + legendColorBackground: '#fff', + + // Text + title: ['title'], + beforeBody: [], + body: [{ + before: [], + lines: ['label'], + after: [] + }], + afterBody: [], + footer: ['footer'], + caretPadding: 2, + labelTextColors: ['#fff'], + labelColors: [{ + borderColor: 'rgb(255, 0, 0)', + backgroundColor: 'rgb(0, 255, 0)' + }, { + borderColor: 'rgb(0, 0, 255)', + backgroundColor: 'rgb(0, 255, 255)' + }] + }; + }; + var drawBody = [ + {name: 'save', args: []}, + {name: 'setFillStyle', args: ['rgba(0,0,0,0.8)']}, + {name: 'setStrokeStyle', args: ['#aaa']}, + {name: 'setLineWidth', args: [1]}, + {name: 'beginPath', args: []}, + {name: 'moveTo', args: [106, 100]}, + {name: 'lineTo', args: [106, 100]}, + {name: 'lineTo', args: [111, 95]}, + {name: 'lineTo', args: [116, 100]}, + {name: 'lineTo', args: [194, 100]}, + {name: 'quadraticCurveTo', args: [200, 100, 200, 106]}, + {name: 'lineTo', args: [200, 194]}, + {name: 'quadraticCurveTo', args: [200, 200, 194, 200]}, + {name: 'lineTo', args: [106, 200]}, + {name: 'quadraticCurveTo', args: [100, 200, 100, 194]}, + {name: 'lineTo', args: [100, 106]}, + {name: 'quadraticCurveTo', args: [100, 100, 106, 100]}, + {name: 'closePath', args: []}, + {name: 'fill', args: []}, + {name: 'stroke', args: []} + ]; + + var mockContext = window.createMockContext(); + var tooltip = new Chart.Tooltip({ + _options: globalDefaults.tooltips, + _chart: { + ctx: mockContext, + } + }); + + it('Should go left', function() { + mockContext.resetCalls(); + tooltip._view = makeView('left', 'left', 'left'); + tooltip.draw(); + + expect(mockContext.getCalls()).toEqual(Array.prototype.concat(drawBody, [ + {name: 'setFillStyle', args: ['#fff']}, + {name: 'fillText', args: ['title', 105, 105]}, + {name: 'setFillStyle', args: ['#fff']}, + {name: 'setFillStyle', args: ['#fff']}, + {name: 'fillText', args: ['label', 105, 123]}, + {name: 'setFillStyle', args: ['#fff']}, + {name: 'fillText', args: ['footer', 105, 141]}, + {name: 'restore', args: []} + ])); + }); + + it('Should go right', function() { + mockContext.resetCalls(); + tooltip._view = makeView('right', 'right', 'right'); + tooltip.draw(); + + expect(mockContext.getCalls()).toEqual(Array.prototype.concat(drawBody, [ + {name: 'setFillStyle', args: ['#fff']}, + {name: 'fillText', args: ['title', 195, 105]}, + {name: 'setFillStyle', args: ['#fff']}, + {name: 'setFillStyle', args: ['#fff']}, + {name: 'fillText', args: ['label', 195, 123]}, + {name: 'setFillStyle', args: ['#fff']}, + {name: 'fillText', args: ['footer', 195, 141]}, + {name: 'restore', args: []} + ])); + }); + + it('Should center', function() { + mockContext.resetCalls(); + tooltip._view = makeView('center', 'center', 'center'); + tooltip.draw(); + + expect(mockContext.getCalls()).toEqual(Array.prototype.concat(drawBody, [ + {name: 'setFillStyle', args: ['#fff']}, + {name: 'fillText', args: ['title', 150, 105]}, + {name: 'setFillStyle', args: ['#fff']}, + {name: 'setFillStyle', args: ['#fff']}, + {name: 'fillText', args: ['label', 150, 123]}, + {name: 'setFillStyle', args: ['#fff']}, + {name: 'fillText', args: ['footer', 150, 141]}, + {name: 'restore', args: []} + ])); + }); + + it('Should allow mixed', function() { + mockContext.resetCalls(); + tooltip._view = makeView('right', 'center', 'left'); + tooltip.draw(); + + expect(mockContext.getCalls()).toEqual(Array.prototype.concat(drawBody, [ + {name: 'setFillStyle', args: ['#fff']}, + {name: 'fillText', args: ['title', 195, 105]}, + {name: 'setFillStyle', args: ['#fff']}, + {name: 'setFillStyle', args: ['#fff']}, + {name: 'fillText', args: ['label', 150, 123]}, + {name: 'setFillStyle', args: ['#fff']}, + {name: 'fillText', args: ['footer', 105, 141]}, + {name: 'restore', args: []} + ])); + }); + }); });