]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Improve tooltip minification
authorEvert Timberg <evert.timberg+github@gmail.com>
Sat, 28 May 2016 23:39:15 +0000 (19:39 -0400)
committerEvert Timberg <evert.timberg+github@gmail.com>
Sat, 28 May 2016 23:39:15 +0000 (19:39 -0400)
src/core/core.helpers.js
src/core/core.tooltip.js

index 17aa0c9b8f3a7b167b9192caf909ec35aa5b7934..958b0bd1086947402017bda994cf80f7d5bb4c0a 100644 (file)
@@ -953,17 +953,6 @@ module.exports = function(Chart) {
 
                return true;
        };
-       helpers.pushAllIfDefined = function(element, array) {
-               if (typeof element === "undefined") {
-                       return;
-               }
-
-               if (helpers.isArray(element)) {
-                       array.push.apply(array, element);
-               } else {
-                       array.push(element);
-               }
-       };
        helpers.callCallback = function(fn, args, _tArg) {
                if (fn && typeof fn.call === 'function') {
                        fn.apply(_tArg, args);
index 3b594c38c094ee95bf4b2639749de787559a009a..61c3e9a0b3f189b60da441b6012b2313ef2e4f91 100644 (file)
@@ -35,12 +35,16 @@ module.exports = function(Chart) {
                        title: function(tooltipItems, data) {
                                // Pick first xLabel for now
                                var title = '';
+                               var labels = data.labels;
+                               var labelCount = labels ? labels.length : 0;
 
                                if (tooltipItems.length > 0) {
-                                       if (tooltipItems[0].xLabel) {
-                                               title = tooltipItems[0].xLabel;
-                                       } else if (data.labels.length > 0 && tooltipItems[0].index < data.labels.length) {
-                                               title = data.labels[tooltipItems[0].index];
+                                       var item = tooltipItems[0];
+
+                                       if (item.xLabel) {
+                                               title = item.xLabel;
+                                       } else if (labelCount > 0 && item.index < labelCount) {
+                                               title = labels[item.index];
                                        }
                                }
 
@@ -91,12 +95,62 @@ module.exports = function(Chart) {
                return base;
        }
 
+       function getAveragePosition(elements) {
+               if (!elements.length) {
+                       return false;
+               }
+
+               var i, len;
+               var xPositions = [];
+               var yPositions = [];
+
+               for (i = 0, len = elements.length; i < len; ++i) {
+                       var el = elements[i];
+                       if (el && el.hasValue()){
+                               var pos = el.tooltipPosition();
+                               xPositions.push(pos.x);
+                               yPositions.push(pos.y);
+                       }
+               }
+
+               var x = 0,
+                       y = 0;
+               for (i = 0, len - xPositions.length; i < len; ++i) {
+                       x += xPositions[i];
+                       y += yPositions[i];
+               }
+
+               return {
+                       x: Math.round(x / xPositions.length),
+                       y: Math.round(y / xPositions.length)
+               };
+       }
+
+       // Private helper to create a tooltip iteam model
+       // @param element : the chart element (point, arc, bar) to create the tooltip item for
+       // @return : new tooltip item
+       function createTooltipItem(element) {
+               var xScale = element._xScale;
+               var yScale = element._yScale || element._scale; // handle radar || polarArea charts
+               var index = element._index,
+                       datasetIndex = element._datasetIndex;
+
+               return {
+                       xLabel: xScale ? xScale.getLabelForIndex(index, datasetIndex) : '',
+                       yLabel: yScale ? yScale.getLabelForIndex(index, datasetIndex) : '',
+                       index: index,
+                       datasetIndex: datasetIndex
+               };
+       }
+
        Chart.Tooltip = Chart.Element.extend({
                initialize: function() {
+                       var me = this;
                        var globalDefaults = Chart.defaults.global;
-                       var tooltipOpts = this._options;
+                       var tooltipOpts = me._options;
+                       var getValueOrDefault = helpers.getValueOrDefault;
 
-                       helpers.extend(this, {
+                       helpers.extend(me, {
                                _model: {
                                        // Positioning
                                        xPadding: tooltipOpts.xPadding,
@@ -106,26 +160,26 @@ module.exports = function(Chart) {
 
                                        // Body
                                        bodyColor: tooltipOpts.bodyColor,
-                                       _bodyFontFamily: helpers.getValueOrDefault(tooltipOpts.bodyFontFamily, globalDefaults.defaultFontFamily),
-                                       _bodyFontStyle: helpers.getValueOrDefault(tooltipOpts.bodyFontStyle, globalDefaults.defaultFontStyle),
+                                       _bodyFontFamily: getValueOrDefault(tooltipOpts.bodyFontFamily, globalDefaults.defaultFontFamily),
+                                       _bodyFontStyle: getValueOrDefault(tooltipOpts.bodyFontStyle, globalDefaults.defaultFontStyle),
                                        _bodyAlign: tooltipOpts.bodyAlign,
-                                       bodyFontSize: helpers.getValueOrDefault(tooltipOpts.bodyFontSize, globalDefaults.defaultFontSize),
+                                       bodyFontSize: getValueOrDefault(tooltipOpts.bodyFontSize, globalDefaults.defaultFontSize),
                                        bodySpacing: tooltipOpts.bodySpacing,
 
                                        // Title
                                        titleColor: tooltipOpts.titleColor,
-                                       _titleFontFamily: helpers.getValueOrDefault(tooltipOpts.titleFontFamily, globalDefaults.defaultFontFamily),
-                                       _titleFontStyle: helpers.getValueOrDefault(tooltipOpts.titleFontStyle, globalDefaults.defaultFontStyle),
-                                       titleFontSize: helpers.getValueOrDefault(tooltipOpts.titleFontSize, globalDefaults.defaultFontSize),
+                                       _titleFontFamily: getValueOrDefault(tooltipOpts.titleFontFamily, globalDefaults.defaultFontFamily),
+                                       _titleFontStyle: getValueOrDefault(tooltipOpts.titleFontStyle, globalDefaults.defaultFontStyle),
+                                       titleFontSize: getValueOrDefault(tooltipOpts.titleFontSize, globalDefaults.defaultFontSize),
                                        _titleAlign: tooltipOpts.titleAlign,
                                        titleSpacing: tooltipOpts.titleSpacing,
                                        titleMarginBottom: tooltipOpts.titleMarginBottom,
 
                                        // Footer
                                        footerColor: tooltipOpts.footerColor,
-                                       _footerFontFamily: helpers.getValueOrDefault(tooltipOpts.footerFontFamily, globalDefaults.defaultFontFamily),
-                                       _footerFontStyle: helpers.getValueOrDefault(tooltipOpts.footerFontStyle, globalDefaults.defaultFontStyle),
-                                       footerFontSize: helpers.getValueOrDefault(tooltipOpts.footerFontSize, globalDefaults.defaultFontSize),
+                                       _footerFontFamily: getValueOrDefault(tooltipOpts.footerFontFamily, globalDefaults.defaultFontFamily),
+                                       _footerFontStyle: getValueOrDefault(tooltipOpts.footerFontStyle, globalDefaults.defaultFontStyle),
+                                       footerFontSize: getValueOrDefault(tooltipOpts.footerFontSize, globalDefaults.defaultFontSize),
                                        _footerAlign: tooltipOpts.footerAlign,
                                        footerSpacing: tooltipOpts.footerSpacing,
                                        footerMarginTop: tooltipOpts.footerMarginTop,
@@ -143,9 +197,13 @@ module.exports = function(Chart) {
                // Get the title
                // Args are: (tooltipItem, data)
                getTitle: function() {
-                       var beforeTitle = this._options.callbacks.beforeTitle.apply(this, arguments),
-                               title = this._options.callbacks.title.apply(this, arguments),
-                               afterTitle = this._options.callbacks.afterTitle.apply(this, arguments);
+                       var me = this;
+                       var opts = me._options;
+                       var callbacks = opts.callbacks;
+
+                       var beforeTitle = callbacks.beforeTitle.apply(me, arguments),
+                               title = callbacks.title.apply(me, arguments),
+                               afterTitle = callbacks.afterTitle.apply(me, arguments);
 
                        var lines = [];
                        lines = pushOrConcat(lines, beforeTitle);
@@ -157,12 +215,15 @@ module.exports = function(Chart) {
 
                // Args are: (tooltipItem, data)
                getBeforeBody: function() {
-                       var lines = this._options.callbacks.beforeBody.apply(this, arguments);
+                       var me = this;
+                       var lines = me._options.callbacks.beforeBody.apply(me, arguments);
                        return helpers.isArray(lines) ? lines : lines !== undefined ? [lines] : [];
                },
 
                // Args are: (tooltipItem, data)
                getBody: function(tooltipItems, data) {
+                       var me = this;
+                       var callbacks = me._options.callbacks;
                        var bodyItems = [];
 
                        helpers.each(tooltipItems, function(tooltipItem) {
@@ -171,28 +232,32 @@ module.exports = function(Chart) {
                                        lines: [],
                                        after: []
                                };
-                               helpers.pushAllIfDefined(this._options.callbacks.beforeLabel.call(this, tooltipItem, data), bodyItem.before);
-                               helpers.pushAllIfDefined(this._options.callbacks.label.call(this, tooltipItem, data), bodyItem.lines);
-                               helpers.pushAllIfDefined(this._options.callbacks.afterLabel.call(this, tooltipItem, data), bodyItem.after);
+                               pushOrConcat(bodyItem.before, callbacks.beforeLabel.call(me, tooltipItem, data));
+                               pushOrConcat(bodyItem.lines, callbacks.label.call(me, tooltipItem, data));
+                               pushOrConcat(bodyItem.after, callbacks.afterLabel.call(me, tooltipItem, data));
 
                                bodyItems.push(bodyItem);
-                       }, this);
+                       });
 
                        return bodyItems;
                },
 
                // Args are: (tooltipItem, data)
                getAfterBody: function() {
-                       var lines = this._options.callbacks.afterBody.apply(this, arguments);
+                       var me = this;
+                       var lines = me._options.callbacks.afterBody.apply(me, arguments);
                        return helpers.isArray(lines) ? lines : lines !== undefined ? [lines] : [];
                },
 
                // Get the footer and beforeFooter and afterFooter lines
                // Args are: (tooltipItem, data)
                getFooter: function() {
-                       var beforeFooter = this._options.callbacks.beforeFooter.apply(this, arguments);
-                       var footer = this._options.callbacks.footer.apply(this, arguments);
-                       var afterFooter = this._options.callbacks.afterFooter.apply(this, arguments);
+                       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, beforeFooter);
@@ -202,90 +267,42 @@ module.exports = function(Chart) {
                        return lines;
                },
 
-               getAveragePosition: function(elements) {
-
-                       if (!elements.length) {
-                               return false;
-                       }
-
-                       var xPositions = [];
-                       var yPositions = [];
-
-                       helpers.each(elements, function(el) {
-                               if (el && el.hasValue()){
-                                       var pos = el.tooltipPosition();
-                                       xPositions.push(pos.x);
-                                       yPositions.push(pos.y);
-                               }
-                       });
-
-                       var x = 0,
-                               y = 0;
-                       for (var i = 0; i < xPositions.length; i++) {
-                               x += xPositions[i];
-                               y += yPositions[i];
-                       }
+               update: function(changed) {
+                       var me = this;
+                       var opts = me._options;
+                       var model = me._model;
+                       var active = me._active;
+                       var data = me._data;
+                       var chartInstance = me._chartInstance;
 
-                       return {
-                               x: Math.round(x / xPositions.length),
-                               y: Math.round(y / xPositions.length)
-                       };
+                       var i, len;
 
-               },
+                       if (active.length) {
+                               model.opacity = 1;
 
-               update: function(changed) {
-                       if (this._active.length) {
-                               this._model.opacity = 1;
-
-                               var element = this._active[0],
+                               var element = active[0],
                                        labelColors = [],
-                                       tooltipPosition;
+                                       tooltipPosition = tooltipPosition = getAveragePosition(active);
 
                                var tooltipItems = [];
+                               for (var i = 0, len = active.length; i < len; ++i) {
+                                       tooltipItems.push(createTooltipItem(active[i]));
+                               }
 
-                               if (this._options.mode === 'single') {
-                                       var yScale = element._yScale || element._scale; // handle radar || polarArea charts
-                                       tooltipItems.push({
-                                               xLabel: element._xScale ? element._xScale.getLabelForIndex(element._index, element._datasetIndex) : '',
-                                               yLabel: yScale ? yScale.getLabelForIndex(element._index, element._datasetIndex) : '',
-                                               index: element._index,
-                                               datasetIndex: element._datasetIndex
-                                       });
-                                       tooltipPosition = this.getAveragePosition(this._active);
-                               } else {
-                                       helpers.each(this._data.datasets, function(dataset, datasetIndex) {
-                                               if (!this._chartInstance.isDatasetVisible(datasetIndex)) {
-                                                       return;
-                                               }
-
-                                               var meta = this._chartInstance.getDatasetMeta(datasetIndex);
-                                               var currentElement = meta.data[element._index];
-                                               if (currentElement) {
-                                                       var yScale = element._yScale || element._scale; // handle radar || polarArea charts
-
-                                                       tooltipItems.push({
-                                                               xLabel: currentElement._xScale ? currentElement._xScale.getLabelForIndex(currentElement._index, currentElement._datasetIndex) : '',
-                                                               yLabel: yScale ? yScale.getLabelForIndex(currentElement._index, currentElement._datasetIndex) : '',
-                                                               index: element._index,
-                                                               datasetIndex: datasetIndex
-                                                       });
-                                               }
-                                       }, this);
-
+                               // If there is more than one item, show color items
+                               if (active.length > 1) {
                                        helpers.each(tooltipItems, function(tooltipItem) {
-                                               labelColors.push(this._options.callbacks.labelColor.call(this, tooltipItem, this._chartInstance));
-                                       }, this);
-
-                                       tooltipPosition = this.getAveragePosition(this._active);
+                                               labelColors.push(opts.callbacks.labelColor.call(me, tooltipItem, chartInstance));
+                                       });
                                }
 
                                // Build the Text Lines
-                               helpers.extend(this._model, {
-                                       title: this.getTitle(tooltipItems, this._data),
-                                       beforeBody: this.getBeforeBody(tooltipItems, this._data),
-                                       body: this.getBody(tooltipItems, this._data),
-                                       afterBody: this.getAfterBody(tooltipItems, this._data),
-                                       footer: this.getFooter(tooltipItems, this._data),
+                               helpers.extend(model, {
+                                       title: me.getTitle(tooltipItems, data),
+                                       beforeBody: me.getBeforeBody(tooltipItems, data),
+                                       body: me.getBody(tooltipItems, data),
+                                       afterBody: me.getAfterBody(tooltipItems, data),
+                                       footer: me.getFooter(tooltipItems, data),
                                        x: Math.round(tooltipPosition.x),
                                        y: Math.round(tooltipPosition.y),
                                        caretPadding: helpers.getValueOrDefault(tooltipPosition.padding, 2),
@@ -293,88 +310,103 @@ module.exports = function(Chart) {
                                });
 
                                // We need to determine alignment of
-                               var tooltipSize = this.getTooltipSize(this._model);
-                               this.determineAlignment(tooltipSize); // Smart Tooltip placement to stay on the canvas
+                               var tooltipSize = me.getTooltipSize(model);
+                               me.determineAlignment(tooltipSize); // Smart Tooltip placement to stay on the canvas
 
-                               helpers.extend(this._model, this.getBackgroundPoint(this._model, tooltipSize));
+                               helpers.extend(model, me.getBackgroundPoint(model, tooltipSize));
                        } else {
-                               this._model.opacity = 0;
+                               me._model.opacity = 0;
                        }
 
-                       if (changed && this._options.custom) {
-                               this._options.custom.call(this, this._model);
+                       if (changed && opts.custom) {
+                               opts.custom.call(me, model);
                        }
 
-                       return this;
+                       return me;
                },
                getTooltipSize: function getTooltipSize(vm) {
-                       var ctx = this._chart.ctx;
+                       var me = this;
+                       var ctx = me._chart.ctx;
 
                        var size = {
                                height: vm.yPadding * 2, // Tooltip Padding
                                width: 0
                        };
 
-
-                       var combinedBodyLength = vm.body.reduce(function(count, bodyItem) {
+                       // Count of all lines in the body
+                       var body = vm.body;
+                       var combinedBodyLength = body.reduce(function(count, bodyItem) {
                                return count + bodyItem.before.length + bodyItem.lines.length + bodyItem.after.length;
                        }, 0);
-                       // Count in before and after body sections
                        combinedBodyLength += 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
+                       var titleLineCount = vm.title.length;
+                       var footerLineCount = vm.footer.length;
+                       var titleFontSize = vm.titleFontSize,
+                               bodyFontSize = vm.bodyFontSize,
+                               footerFontSize = vm.footerFontSize;
+
+                       size.height += titleLineCount * titleFontSize; // Title Lines
+                       size.height += (titleLineCount - 1) * vm.titleSpacing; // Title Line Spacing
+                       size.height += titleLineCount ? vm.titleMarginBottom : 0; // Title's bottom Margin
+                       size.height += combinedBodyLength * 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);
-                       });
+                       size.height += footerLineCount ? vm.footerMarginTop : 0; // Footer Margin
+                       size.height += footerLineCount * (footerFontSize); // Footer Lines
+                       size.height += footerLineCount ? (footerLineCount - 1) * vm.footerSpacing : 0; // Footer Line Spacing
+
+                       // Title width
+                       var widthPadding = 0;
+                       var maxLineWidth = function(line) {
+                               size.width = Math.max(size.width, ctx.measureText(line).width + widthPadding);
+                       };
 
-                       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);
-                       });
+                       ctx.font = helpers.fontString(titleFontSize, vm._titleFontStyle, vm._titleFontFamily);
+                       helpers.each(vm.title, maxLineWidth);
 
-                       var _this = this;
-                       var maxBodyWidth = function(line) {
-                               size.width = Math.max(size.width, ctx.measureText(line).width + (_this._options.mode !== 'single' ? (vm.bodyFontSize + 2) : 0));
-                       };
-                       helpers.each(vm.body, function(bodyItem) {
-                               helpers.each(bodyItem.before, maxBodyWidth);
-                               helpers.each(bodyItem.lines, maxBodyWidth);
-                               helpers.each(bodyItem.after, maxBodyWidth);
-                       });
+                       // Body width
+                       ctx.font = helpers.fontString(bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily);
+                       helpers.each(vm.beforeBody.concat(vm.afterBody), maxLineWidth);
 
-                       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);
+                       // Body lines may include some extra width due to the color box
+                       widthPadding = body.length > 1 ? (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, vm._footerFontStyle, vm._footerFontFamily);
+                       helpers.each(vm.footer, maxLineWidth);
+
+                       // Add padding
                        size.width += 2 * vm.xPadding;
 
                        return size;
                },
                determineAlignment: function determineAlignment(size) {
-                       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 me = this;
+                       var model = me._model;
+                       var chart = me._chart;
+                       var chartArea = me._chartInstance.chartArea;
+
+                       if (model.y < size.height) {
+                               model.yAlign = 'top';
+                       } else if (model.y > (chart.height - size.height)) {
+                               model.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 _this = this;
-                       var midX = (this._chartInstance.chartArea.left + this._chartInstance.chartArea.right) / 2;
-                       var midY = (this._chartInstance.chartArea.top + this._chartInstance.chartArea.bottom) / 2;
+                       var midX = (chartArea.left + chartArea.right) / 2;
+                       var midY = (chartArea.top + chartArea.bottom) / 2;
 
-                       if (this._model.yAlign === 'center') {
+                       if (model.yAlign === 'center') {
                                lf = function(x) {
                                        return x <= midX;
                                };
@@ -386,12 +418,12 @@ module.exports = function(Chart) {
                                        return x <= (size.width / 2);
                                };
                                rf = function(x) {
-                                       return x >= (_this._chart.width - (size.width / 2));
+                                       return x >= (chart.width - (size.width / 2));
                                };
                        }
 
                        olf = function(x) {
-                               return x + size.width > _this._chart.width;
+                               return x + size.width > chart.width;
                        };
                        orf = function(x) {
                                return x - size.width < 0;
@@ -400,21 +432,21 @@ module.exports = function(Chart) {
                                return y <= midY ? 'top' : 'bottom';
                        };
 
-                       if (lf(this._model.x)) {
-                               this._model.xAlign = 'left';
+                       if (lf(model.x)) {
+                               model.xAlign = 'left';
 
                                // Is tooltip too wide and goes over the right side of the chart.?
-                               if (olf(this._model.x)) {
-                                       this._model.xAlign = 'center';
-                                       this._model.yAlign = yf(this._model.y);
+                               if (olf(model.x)) {
+                                       model.xAlign = 'center';
+                                       model.yAlign = yf(model.y);
                                }
-                       } else if (rf(this._model.x)) {
-                               this._model.xAlign = 'right';
+                       } else if (rf(model.x)) {
+                               model.xAlign = 'right';
 
                                // Is tooltip too wide and goes outside left edge of canvas?
-                               if (orf(this._model.x)) {
-                                       this._model.xAlign = 'center';
-                                       this._model.yAlign = yf(this._model.y);
+                               if (orf(model.x)) {
+                                       model.xAlign = 'center';
+                                       model.yAlign = yf(model.y);
                                }
                        }
                },
@@ -425,79 +457,96 @@ module.exports = function(Chart) {
                                y: vm.y
                        };
 
-                       if (vm.xAlign === 'right') {
+                       var caretSize = vm.caretSize,
+                               caretPadding = vm.caretPadding,
+                               cornerRadius = vm.cornerRadius,
+                               xAlign = vm.xAlign,
+                               yAlign = vm.yAlign,
+                               paddingAndSize = caretSize + caretPadding,
+                               radiusAndPadding = cornerRadius + caretPadding;
+
+                       if (xAlign === 'right') {
                                pt.x -= size.width;
-                       } else if (vm.xAlign === 'center') {
+                       } else if (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;
+                       if (yAlign === 'top') {
+                               pt.y += paddingAndSize;
+                       } else if (yAlign === 'bottom') {
+                               pt.y -= size.height + paddingAndSize;
                        } 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;
+                       if (yAlign === 'center') {
+                               if (xAlign === 'left') {
+                                       pt.x += paddingAndSize;
+                               } else if (xAlign === 'right') {
+                                       pt.x -= paddingAndSize;
                                }
                        } else {
-                               if (vm.xAlign === 'left') {
-                                       pt.x -= vm.cornerRadius + vm.caretPadding;
-                               } else if (vm.xAlign === 'right') {
-                                       pt.x += vm.cornerRadius + vm.caretPadding;
+                               if (xAlign === 'left') {
+                                       pt.x -= radiusAndPadding;
+                               } else if (xAlign === 'right') {
+                                       pt.x += radiusAndPadding;
                                }
                        }
 
                        return pt;
                },
                drawCaret: function drawCaret(tooltipPoint, size, opacity, caretPadding) {
-                       var vm = this._view;
-                       var ctx = this._chart.ctx;
+                       var me = this;
+                       var vm = me._view;
+                       var ctx = me._chart.ctx;
                        var x1, x2, x3;
                        var y1, y2, y3;
-
-                       if (vm.yAlign === 'center') {
+                       var caretSize = vm.caretSize;
+                       var cornerRadius = vm.cornerRadius;
+                       var xAlign = vm.xAlign,
+                               yAlign = vm.yAlign;
+                       var ptX = tooltipPoint.x,
+                               ptY = tooltipPoint.y;
+                       var width = size.width,
+                               height = size.height;
+
+                       if (yAlign === 'center') {
                                // Left or right side
-                               if (vm.xAlign === 'left') {
-                                       x1 = tooltipPoint.x;
-                                       x2 = x1 - vm.caretSize;
+                               if (xAlign === 'left') {
+                                       x1 = ptX;
+                                       x2 = x1 - caretSize;
                                        x3 = x1;
                                } else {
-                                       x1 = tooltipPoint.x + size.width;
-                                       x2 = x1 + vm.caretSize;
+                                       x1 = ptX + width;
+                                       x2 = x1 + caretSize;
                                        x3 = x1;
                                }
 
-                               y2 = tooltipPoint.y + (size.height / 2);
-                               y1 = y2 - vm.caretSize;
-                               y3 = y2 + vm.caretSize;
+                               y2 = ptY + (height / 2);
+                               y1 = y2 - caretSize;
+                               y3 = y2 + 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;
+                               if (xAlign === 'left') {
+                                       x1 = ptX + cornerRadius;
+                                       x2 = x1 + caretSize;
+                                       x3 = x2 + caretSize;
+                               } else if (xAlign === 'right') {
+                                       x1 = ptX + width - cornerRadius;
+                                       x2 = x1 - caretSize;
+                                       x3 = x2 - caretSize;
                                } else {
-                                       x2 = tooltipPoint.x + (size.width / 2);
-                                       x1 = x2 - vm.caretSize;
-                                       x3 = x2 + vm.caretSize;
+                                       x2 = ptX + (width / 2);
+                                       x1 = x2 - caretSize;
+                                       x3 = x2 + caretSize;
                                }
 
-                               if (vm.yAlign === 'top') {
-                                       y1 = tooltipPoint.y;
-                                       y2 = y1 - vm.caretSize;
+                               if (yAlign === 'top') {
+                                       y1 = ptY;
+                                       y2 = y1 - caretSize;
                                        y3 = y1;
                                } else {
-                                       y1 = tooltipPoint.y + size.height;
-                                       y2 = y1 + vm.caretSize;
+                                       y1 = ptY + height;
+                                       y2 = y1 + caretSize;
                                        y3 = y1;
                                }
                        }
@@ -512,82 +561,96 @@ module.exports = function(Chart) {
                        ctx.fill();
                },
                drawTitle: function drawTitle(pt, vm, ctx, opacity) {
-                       if (vm.title.length) {
+                       var title = vm.title;
+
+                       if (title.length) {
                                ctx.textAlign = vm._titleAlign;
                                ctx.textBaseline = "top";
 
+                               var titleFontSize = vm.titleFontSize,
+                                       titleSpacing = vm.titleSpacing;
+
                                var titleColor = helpers.color(vm.titleColor);
                                ctx.fillStyle = titleColor.alpha(opacity * titleColor.alpha()).rgbString();
-                               ctx.font = helpers.fontString(vm.titleFontSize, vm._titleFontStyle, vm._titleFontFamily);
+                               ctx.font = helpers.fontString(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
+                               var i, len;
+                               for (i = 0, len = title.length; i < len; ++i) {
+                                       ctx.fillText(title[i], pt.x, pt.y);
+                                       pt.y += titleFontSize + titleSpacing; // Line Height and spacing
 
-                                       if (i + 1 === vm.title.length) {
-                                               pt.y += vm.titleMarginBottom - vm.titleSpacing; // If Last, add margin, remove spacing
+                                       if (i + 1 === title.length) {
+                                               pt.y += vm.titleMarginBottom - titleSpacing; // If Last, add margin, remove spacing
                                        }
-                               });
+                               }
                        }
                },
                drawBody: function drawBody(pt, vm, ctx, opacity) {
+                       var me = this;
+                       var bodyFontSize = vm.bodyFontSize;
+                       var bodySpacing = vm.bodySpacing;
+                       var body = vm.body;
+
                        ctx.textAlign = vm._bodyAlign;
                        ctx.textBaseline = "top";
 
                        var bodyColor = helpers.color(vm.bodyColor);
-                       ctx.fillStyle = bodyColor.alpha(opacity * bodyColor.alpha()).rgbString();
-                       ctx.font = helpers.fontString(vm.bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily);
+                       var textColor = bodyColor.alpha(opacity * bodyColor.alpha()).rgbString();
+                       ctx.fillStyle = textColor
+                       ctx.font = helpers.fontString(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;
-                       });
+                       var xLinePadding = 0;
+                       var fillLineOfText = function(line) {
+                               ctx.fillText(line, pt.x + xLinePadding, pt.y);
+                               pt.y += bodyFontSize + bodySpacing;
+                       };
 
-                       helpers.each(vm.body, function(bodyItem, i) {
-                               var _this = this;
-                               var fillLine = function(line) {
-                                       // Body Line
-                                       ctx.fillText(line, pt.x + (_this._options.mode !== 'single' ? (vm.bodyFontSize + 2) : 0), pt.y);
-                                       pt.y += vm.bodyFontSize + vm.bodySpacing;
-                               };
+                       // Before body lines
+                       helpers.each(vm.beforeBody, fillLineOfText);
 
-                               helpers.each(bodyItem.before, fillLine);
+                       var drawColorBoxes = body.length > 1;
+                       xLinePadding = drawColorBoxes ? (bodyFontSize + 2) : 0
+                       
+                       // Draw body lines now
+                       helpers.each(body, function(bodyItem, i) {
+                               helpers.each(bodyItem.before, fillLineOfText);
 
                                helpers.each(bodyItem.lines, function(line) {
                                        // Draw Legend-like boxes if needed
-                                       if (this._options.mode !== 'single') {
+                                       if (drawColorBoxes) {
                                                // 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);
+                                               ctx.fillRect(pt.x, pt.y, bodyFontSize, bodyFontSize);
 
                                                // Border
                                                ctx.strokeStyle = helpers.color(vm.labelColors[i].borderColor).alpha(opacity).rgbaString();
-                                               ctx.strokeRect(pt.x, pt.y, vm.bodyFontSize, vm.bodyFontSize);
+                                               ctx.strokeRect(pt.x, pt.y, bodyFontSize, 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.fillRect(pt.x + 1, pt.y + 1, bodyFontSize - 2, bodyFontSize - 2);
 
-                                               ctx.fillStyle = helpers.color(vm.bodyColor).alpha(opacity).rgbaString(); // Return fill style for text
+                                               ctx.fillStyle = textColor;
                                        }
 
-                                       fillLine(line);
-                               }, this);
+                                       fillLineOfText(line);
+                               });
                                
-                               helpers.each(bodyItem.after, fillLine);
-                       }, this);
-
-                       // After Body
-                       helpers.each(vm.afterBody, function(afterBody) {
-                               ctx.fillText(afterBody, pt.x, pt.y);
-                               pt.y += vm.bodyFontSize;
+                               helpers.each(bodyItem.after, fillLineOfText);
                        });
 
-                       pt.y -= vm.bodySpacing; // Remove last body spacing
+                       // 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: function drawFooter(pt, vm, ctx, opacity) {
-                       if (vm.footer.length) {
+                       var footer = vm.footer;
+
+                       if (footer.length) {
                                pt.y += vm.footerMarginTop;
 
                                ctx.textAlign = vm._footerAlign;
@@ -597,8 +660,8 @@ module.exports = function(Chart) {
                                ctx.fillStyle = footerColor.alpha(opacity * footerColor.alpha()).rgbString();
                                ctx.font = helpers.fontString(vm.footerFontSize, vm._footerFontStyle, vm._footerFontFamily);
 
-                               helpers.each(vm.footer, function(footer) {
-                                       ctx.fillText(footer, pt.x, pt.y);
+                               helpers.each(footer, function(line) {
+                                       ctx.fillText(line, pt.x, pt.y);
                                        pt.y += vm.footerFontSize + vm.footerSpacing;
                                });
                        }
@@ -611,7 +674,6 @@ module.exports = function(Chart) {
                                return;
                        }
 
-                       var caretPadding = vm.caretPadding;
                        var tooltipSize = this.getTooltipSize(vm);
                        var pt = {
                                x: vm.x,
@@ -629,7 +691,7 @@ module.exports = function(Chart) {
                                ctx.fill();
 
                                // Draw Caret
-                               this.drawCaret(pt, tooltipSize, opacity, caretPadding);
+                               this.drawCaret(pt, tooltipSize, opacity, vm.caretPadding);
 
                                // Draw Title, Body, and Footer
                                pt.x += vm.xPadding;