From ef5cec7f5e7ea86e1ba72a2ad3b12079960e971a Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Tue, 6 Oct 2015 20:40:25 -0600 Subject: [PATCH] Tooltips now using new lineArray format --- samples/line.html | 31 +- src/core/core.controller.js | 23 +- src/core/core.helpers.js | 59 +-- src/core/core.js | 17 +- src/core/core.scale.js | 45 +- src/core/core.tooltip.js | 688 ++++++++++++++++++++----------- src/scales/scale.radialLinear.js | 6 +- 7 files changed, 549 insertions(+), 320 deletions(-) diff --git a/samples/line.html b/samples/line.html index 9e0cf17d7..546f69acd 100644 --- a/samples/line.html +++ b/samples/line.html @@ -13,7 +13,7 @@ -
+

@@ -55,6 +55,35 @@ }, options: { responsive: true, + tooltips: { + mode: 'label', + callbacks: { + // beforeTitle: function() { + // return '...beforeTitle'; + // }, + // afterTitle: function() { + // return '...afterTitle'; + // }, + // beforeBody: function() { + // return '...beforeBody'; + // }, + // afterBody: function() { + // return '...afterBody'; + // }, + // beforeFooter: function() { + // return '...beforeFooter'; + // }, + // footer: function() { + // return 'Footer'; + // }, + // afterFooter: function() { + // return '...afterFooter'; + // }, + } + }, + hover: { + mode: 'label' + }, scales: { xAxes: [{ display: true, diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 6d8d5e25e..ccb3a7a06 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -319,7 +319,7 @@ }, generateLegend: function generateLegend() { - return helpers.template(this.options.legendTemplate, this); + return this.options.legendCallback(this); }, destroy: function destroy() { @@ -364,9 +364,9 @@ eventHandler: function eventHandler(e) { this.lastActive = this.lastActive || []; - // Find Active Elements + // Find Active Elements for hover and tooltips if (e.type == 'mouseout') { - this.active = []; + this.active = this.tooltipActive = []; } else { this.active = function() { switch (this.options.hover.mode) { @@ -380,6 +380,18 @@ return e; } }.call(this); + this.tooltipActive = function() { + switch (this.options.tooltips.mode) { + case 'single': + return this.getElementAtEvent(e); + case 'label': + return this.getElementsAtEvent(e); + case 'dataset': + return this.getDatasetAtEvent(e); + default: + return e; + } + }.call(this); } // On Hover hook @@ -395,6 +407,7 @@ var dataset; var index; + // Remove styling for last active (even if it may still be active) if (this.lastActive.length) { switch (this.options.hover.mode) { @@ -437,11 +450,11 @@ this.tooltip.initialize(); // Active - if (this.active.length) { + if (this.tooltipActive.length) { this.tooltip._model.opacity = 1; helpers.extend(this.tooltip, { - _active: this.active, + _active: this.tooltipActive, }); this.tooltip.update(); diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index 8c848deba..d2f6c4d25 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -135,7 +135,7 @@ base[key].push(helpers.configMerge(valueObj.type ? Chart.scaleService.getScaleDefaults(valueObj.type) : {}, valueObj)); } else if (valueObj.type !== base[key][index].type) { // Type changed. Bring in the new defaults before we bring in valueObj so that valueObj can override the correct scale defaults - base[key][index] = helpers.configMerge(base[key][index], valueObj.type ? Chart.scaleService.getScaleDefaults(valueObj.type) : {}, valueObj) + base[key][index] = helpers.configMerge(base[key][index], valueObj.type ? Chart.scaleService.getScaleDefaults(valueObj.type) : {}, valueObj); } else { // Type is the same base[key][index] = helpers.configMerge(base[key][index], valueObj); @@ -272,7 +272,7 @@ }, log10 = helpers.log10 = function(x) { if (Math.log10) { - return Math.log10(x) + return Math.log10(x); } else { return Math.log(x) / Math.LN10; } @@ -382,57 +382,6 @@ return niceFraction * Math.pow(10, exponent); }, - /* jshint ignore:start */ - // Blows up jshint errors based on the new Function constructor - //Templating methods - //Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/ - templateStringCache = {}, - template = helpers.template = function(templateString, valuesObject) { - - // If templateString is function rather than string-template - call the function for valuesObject - - if (templateString instanceof Function) { - return templateString(valuesObject); - } - - function tmpl(str, data) { - // Figure out if we're getting a template, or if we need to - // load the template - and be sure to cache the result. - var fn; - - if (templateStringCache.hasOwnProperty(str)) { - fn = templateStringCache[str]; - } else { - // Generate a reusable function that will serve as a template - // generator (and which will be cached). - var functionCode = "var p=[],print=function(){p.push.apply(p,arguments);};" + - - // Introduce the data as local variables using with(){} - "with(obj){p.push('" + - - // Convert the template into pure JavaScript - str - .replace(/[\r\t\n]/g, " ") - .split("<%").join("\t") - .replace(/((^|%>)[^\t]*)'/g, "$1\r") - .replace(/\t=(.*?)%>/g, "',$1,'") - .split("\t").join("');") - .split("%>").join("p.push('") - .split("\r").join("\\'") + - "');}return p.join('');"; - fn = new Function("obj", functionCode); - - // Cache the result - templateStringCache[str] = fn; - } - - // Provide some basic currying to the user - return data ? fn(data) : fn; - } - return tmpl(templateString, valuesObject); - }, - /* jshint ignore:end */ - //--Animation methods //Easing functions adapted from Robert Penner's easing equations //http://www.robertpenner.com/easing/ easingEffects = helpers.easingEffects = { @@ -837,7 +786,7 @@ // can use classlist hiddenIframe.classlist.add(hiddenIframeClass); } else { - hiddenIframe.setAttribute('class', hiddenIframeClass) + hiddenIframe.setAttribute('class', hiddenIframeClass); } // Set the style @@ -860,7 +809,7 @@ if (callback) { callback(); } - } + }; }, removeResizeListener = helpers.removeResizeListener = function(node) { var hiddenIframe = node.querySelector('.chartjs-hidden-iframe'); diff --git a/src/core/core.js b/src/core/core.js index d930c024f..a4eb71294 100755 --- a/src/core/core.js +++ b/src/core/core.js @@ -92,8 +92,21 @@ // Element defaults defined in element extensions elements: {}, - // Legend template string - legendTemplate: "", + // Legend callback string + legendCallback: function(chart) { + var text = []; + text.push(''); + + return text.join(""); + } }, }; diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 40c73b4f5..295346a68 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -47,7 +47,9 @@ padding: 10, reverse: false, show: true, - template: "<%=value%>", + callback: function(value) { + return value; + }, }, }; @@ -126,14 +128,12 @@ convertTicksToLabels: function() { // Convert ticks to strings this.ticks = this.ticks.map(function(numericalTick, index, ticks) { - if (this.options.ticks.userCallback) { - return this.options.ticks.userCallback(numericalTick, index, ticks); - } else { - return helpers.template(this.options.ticks.template, { - value: numericalTick - }); - } - }, this); + if (this.options.ticks.userCallback) { + return this.options.ticks.userCallback(numericalTick, index, ticks); + } + return this.options.ticks.callback(numericalTick); + }, + this); }, afterTickToLabelConversion: helpers.noop, @@ -235,13 +235,13 @@ } // Are we showing a title for the scale? - if (this.options.scaleLabel.show) { - if (this.isHorizontal()) { - this.minSize.height += (this.options.scaleLabel.fontSize * 1.5); - } else { - this.minSize.width += (this.options.scaleLabel.fontSize * 1.5); - } - } + if (this.options.scaleLabel.show) { + if (this.isHorizontal()) { + this.minSize.height += (this.options.scaleLabel.fontSize * 1.5); + } else { + this.minSize.width += (this.options.scaleLabel.fontSize * 1.5); + } + } if (this.options.ticks.show && this.options.display) { // Don't bother fitting the ticks if we are not showing them @@ -304,11 +304,22 @@ }, afterFit: helpers.noop, + + + + // Shared Methods isHorizontal: function() { return this.options.position == "top" || this.options.position == "bottom"; }, + getLabelForIndex: function(index, datasetIndex) { + if (this.isHorizontal()) { + return this.data.datasets[datasetIndex].label || this.data.labels[index]; + } + return this.data.datasets[datasetIndex].data[index]; + }, + // Used to get data value locations. Value can either be an index or a numerical value getPixelForValue: helpers.noop, @@ -494,7 +505,7 @@ } } - + this.ctx.translate(xLabelValue, yLabelValue); this.ctx.rotate(helpers.toRadians(this.labelRotation) * -1); this.ctx.font = this.font; diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 92e2b1364..38714968b 100644 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -9,33 +9,50 @@ Chart.defaults.global.tooltips = { enabled: true, custom: null, + mode: 'single', backgroundColor: "rgba(0,0,0,0.8)", - fontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", - fontSize: 10, - fontStyle: "normal", - fontColor: "#fff", titleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", titleFontSize: 12, titleFontStyle: "bold", - titleFontColor: "#fff", + titleColor: "#fff", + titleAlign: "left", + bodyFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + bodyFontSize: 12, + bodyFontStyle: "normal", + bodyColor: "#fff", + bodyAlign: "left", + footerFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + footerFontSize: 12, + footerFontStyle: "bold", + footerColor: "#fff", + footerAlign: "left", yPadding: 6, xPadding: 6, - caretSize: 8, + caretSize: 5, cornerRadius: 6, xOffset: 10, - template: [ - '<% if(label){ %>', - '<%=label %>: ', - '<% } %>', - '<%=value %>', - ].join(''), - multiTemplate: [ - '<%if (datasetLabel){ %>', - '<%=datasetLabel %>: ', - '<% } %>', - '<%=value %>' - ].join(''), multiKeyBackground: '#fff', + callbacks: { + beforeTitle: helpers.noop, + title: function(xLabel, yLabel, index, datasetIndex, data) { + return data.datasets[datasetIndex].label; + }, + afterTitle: helpers.noop, + + beforeBody: helpers.noop, + + beforeLabel: helpers.noop, + label: function(xLabel, yLabel, index, datasetIndex, data) { + return xLabel + ': ' + yLabel; + }, + afterLabel: helpers.noop, + + afterBody: helpers.noop, + + beforeFooter: helpers.noop, + footer: helpers.noop, + afterFooter: helpers.noop, + }, }; Chart.Tooltip = Chart.Element.extend({ @@ -48,20 +65,29 @@ yPadding: options.tooltips.yPadding, xOffset: options.tooltips.xOffset, - // Labels - textColor: options.tooltips.fontColor, - _fontFamily: options.tooltips.fontFamily, - _fontStyle: options.tooltips.fontStyle, - fontSize: options.tooltips.fontSize, + // Body + bodyColor: options.tooltips.bodyColor, + _bodyFontFamily: options.tooltips.bodyFontFamily, + _bodyFontStyle: options.tooltips.bodyFontStyle, + bodyFontSize: options.tooltips.bodyFontSize, + _bodposition: options.tooltips.bodposition, // Title - titleTextColor: options.tooltips.titleFontColor, + titleColor: options.tooltips.titleColor, _titleFontFamily: options.tooltips.titleFontFamily, _titleFontStyle: options.tooltips.titleFontStyle, titleFontSize: options.tooltips.titleFontSize, + _titleAlign: options.tooltips.titleAlign, + + // Footer + footerColor: options.tooltips.footerColor, + _footerFontFamily: options.tooltips.footerFontFamily, + _footerFontStyle: options.tooltips.footerFontStyle, + footerFontSize: options.tooltips.footerFontSize, + _footerAlign: options.tooltips.footerAlign, // Appearance - caretHeight: options.tooltips.caretSize, + caretSize: options.tooltips.caretSize, cornerRadius: options.tooltips.cornerRadius, backgroundColor: options.tooltips.backgroundColor, opacity: 0, @@ -69,141 +95,251 @@ }, }); }, - update: function() { - var ctx = this._chart.ctx; + getTitle: function() { + var beforeTitle = this._options.tooltips.callbacks.beforeTitle.apply(this, arguments), + title = this._options.tooltips.callbacks.title.apply(this, arguments), + afterTitle = this._options.tooltips.callbacks.afterTitle.apply(this, arguments); - switch (this._options.hover.mode) { - case 'single': - helpers.extend(this._model, { - text: helpers.template(this._options.tooltips.template, { - // These variables are available in the template function. Add others here - element: this._active[0], - value: this._data.datasets[this._active[0]._datasetIndex].data[this._active[0]._index], - label: this._active[0]._model.label !== undefined ? this._active[0]._model.label : this._data.labels ? this._data.labels[this._active[0]._index] : '', - }), - }); - - var tooltipPosition = this._active[0].tooltipPosition(); - helpers.extend(this._model, { - x: Math.round(tooltipPosition.x), - y: Math.round(tooltipPosition.y), - caretPadding: tooltipPosition.padding - }); + var lines = []; - break; + if (beforeTitle) { + lines.push(beforeTitle); + } + if (title) { + lines.push(title); + } + if (afterTitle) { + lines.push(afterTitle); + } + return lines; + }, - case 'label': + getBody: function(xLabel, yLabel, index, datasetIndex) { - // Tooltip Content + var lines = []; - var dataArray, - dataIndex; + var beforeBody = this._options.tooltips.callbacks.beforeBody.apply(this, arguments); + if (beforeBody) { + lines.push(beforeBody); + } - var labels = [], - colors = []; + var beforeLabel, + afterLabel, + label; - for (var i = this._data.datasets.length - 1; i >= 0; i--) { - dataArray = this._data.datasets[i].metaData; - dataIndex = helpers.indexOf(dataArray, this._active[0]); - if (dataIndex !== -1) { - break; - } - } + if (helpers.isArray(xLabel)) { - var medianPosition = (function(index) { - // Get all the points at that particular index - var elements = [], - dataCollection, - xPositions = [], - yPositions = [], - xMax, - yMax, - xMin, - yMin; - helpers.each(this._data.datasets, function(dataset) { - dataCollection = dataset.metaData; - if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()) { - elements.push(dataCollection[dataIndex]); - } - }, this); - - // Reverse labels if stacked - helpers.each(this._options.stacked ? elements.reverse() : elements, function(element) { - xPositions.push(element._view.x); - yPositions.push(element._view.y); - - //Include any colour information about the element - labels.push(helpers.template(this._options.tooltips.multiTemplate, { - // These variables are available in the template function. Add others here - element: element, - datasetLabel: this._data.datasets[element._datasetIndex].label, - value: this._data.datasets[element._datasetIndex].data[element._index], - })); - colors.push({ - fill: element._view.backgroundColor, - stroke: element._view.borderColor - }); - - }, this); - - yMin = helpers.min(yPositions); - yMax = helpers.max(yPositions); - - xMin = helpers.min(xPositions); - xMax = helpers.max(xPositions); - - return { - x: (xMin > this._chart.width / 2) ? xMin : xMax, - y: (yMin + yMax) / 2, - }; - }).call(this, dataIndex); - - // Apply for now - helpers.extend(this._model, { - x: medianPosition.x, - y: medianPosition.y, - labels: labels, - title: (function() { - return this._data.timeLabels ? this._data.timeLabels[this._active[0]._index] : - (this._data.labels && this._data.labels.length) ? this._data.labels[this._active[0]._index] : - ''; - }).call(this), - legendColors: colors, - legendBackgroundColor: this._options.tooltips.multiKeyBackground, - }); - - - // Calculate Appearance Tweaks - - this._model.height = (labels.length * this._model.fontSize) + ((labels.length - 1) * (this._model.fontSize / 2)) + (this._model.yPadding * 2) + this._model.titleFontSize * 1.5; - - var titleWidth = ctx.measureText(this._model.title).width, - //Label has a legend square as well so account for this. - labelWidth = helpers.longestText(ctx, this.font, labels) + this._model.fontSize + 3, - longestTextWidth = helpers.max([labelWidth, titleWidth]); - - this._model.width = longestTextWidth + (this._model.xPadding * 2); - - - var halfHeight = this._model.height / 2; - - //Check to ensure the height will fit on the canvas - if (this._model.y - halfHeight < 0) { - this._model.y = halfHeight; - } else if (this._model.y + halfHeight > this._chart.height) { - this._model.y = this._chart.height - halfHeight; - } + var labels = []; - //Decide whether to align left or right based on position on canvas - if (this._model.x > this._chart.width / 2) { - this._model.x -= this._model.xOffset + this._model.width; - } else { - this._model.x += this._model.xOffset; - } - break; + // Run EACH label pair through the label callback this time. + for (var i = 0; i < xLabel.length; i++) { + + beforeLabel = this._options.tooltips.callbacks.beforeLabel(xLabel[i], yLabel[i], index, datasetIndex); + afterLabel = this._options.tooltips.callbacks.afterLabel(xLabel[i], yLabel[i], index, datasetIndex); + + labels.push((beforeLabel ? beforeLabel : '') + this._options.tooltips.callbacks.label(xLabel[i], yLabel[i], index, datasetIndex) + (afterLabel ? afterLabel : '')); + + } + + if (labels.length) { + lines = lines.concat(labels); + } + + } else { + + // Run the single label through the callback + + beforeLabel = this._options.tooltips.callbacks.beforeLabel.apply(this, arguments); + label = this._options.tooltips.callbacks.label.apply(this, arguments); + afterLabel = this._options.tooltips.callbacks.afterLabel.apply(this, arguments); + + if (beforeLabel || label || afterLabel) { + lines.push((beforeLabel ? afterLabel : '') + label + (afterLabel ? afterLabel : '')); + } } + var afterBody = this._options.tooltips.callbacks.afterBody.apply(this, arguments); + if (afterBody) { + lines.push(afterBody); + } + + return lines; + }, + + getFooter: function() { + var beforeFooter = this._options.tooltips.callbacks.beforeFooter.apply(this, arguments), + footer = this._options.tooltips.callbacks.footer.apply(this, arguments), + afterFooter = this._options.tooltips.callbacks.afterFooter.apply(this, arguments); + + var lines = []; + + if (beforeFooter) { + lines.push(beforeFooter); + } + if (footer) { + lines.push(footer); + } + if (afterFooter) { + lines.push(afterFooter); + } + + return lines; + }, + + update: function() { + + var ctx = this._chart.ctx; + + var element = this._active[0], + xLabel, + yLabel, + tooltipPosition; + + if (this._options.tooltips.mode == 'single') { + + xLabel = element._xScale.getLabelForIndex(element._index, element._datasetIndex); + yLabel = element._yScale.getLabelForIndex(element._index, element._datasetIndex); + tooltipPosition = this._active[0].tooltipPosition(); + + } else { + + xLabel = []; + yLabel = []; + helpers.each(this._data.datasets, function(dataset, datasetIndex) { + xLabel.push(element._xScale.getLabelForIndex(element._index, datasetIndex)); + yLabel.push(element._yScale.getLabelForIndex(element._index, datasetIndex)); + }); + tooltipPosition = this._active[0].tooltipPosition(); + + // for (var i = 0; i < this._data.datasets.length; i++) { + // this._data.datasets[i].data[index]; + // }; + + // // Tooltip Content + + // var dataArray, + // dataIndex; + + // var labels = [], + // colors = []; + + // for (var i = this._data.datasets.length - 1; i >= 0; i--) { + // dataArray = this._data.datasets[i].metaData; + // dataIndex = helpers.indexOf(dataArray, this._active[0]); + // if (dataIndex !== -1) { + // break; + // } + // } + + // var medianPosition = (function(index) { + // // Get all the points at that particular index + // var elements = [], + // dataCollection, + // xPositions = [], + // yPositions = [], + // xMax, + // yMax, + // xMin, + // yMin; + // helpers.each(this._data.datasets, function(dataset) { + // dataCollection = dataset.metaData; + // if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()) { + // elements.push(dataCollection[dataIndex]); + // } + // }, this); + + // // Reverse labels if stacked + // helpers.each(this._options.stacked ? elements.reverse() : elements, function(element) { + // xPositions.push(element._view.x); + // yPositions.push(element._view.y); + + // //Include any colour information about the element + // labels.push( + // this._options.tooltips.multiTemplate( + // element, + // this._data.datasets[element._datasetIndex].label, + // this._data.datasets[element._datasetIndex].data[element._index] + // ) + // ); + + // colors.push({ + // fill: element._view.backgroundColor, + // stroke: element._view.borderColor + // }); + + // }, this); + + // yMin = helpers.min(yPositions); + // yMax = helpers.max(yPositions); + + // xMin = helpers.min(xPositions); + // xMax = helpers.max(xPositions); + + // return { + // x: (xMin > this._chart.width / 2) ? xMin : xMax, + // y: (yMin + yMax) / 2, + // }; + // }).call(this, dataIndex); + + // // Apply for now + // helpers.extend(this._model, { + // x: medianPosition.x, + // y: medianPosition.y, + // labels: labels, + // title: (function() { + // return this._data.timeLabels ? this._data.timeLabels[this._active[0]._index] : + // (this._data.labels && this._data.labels.length) ? this._data.labels[this._active[0]._index] : + // ''; + // }).call(this), + // legendColors: colors, + // legendBackgroundColor: this._options.tooltips.multiKeyBackground, + // }); + + + // // Calculate Appearance Tweaks + + // this._model.height = (labels.length * this._model.bodyFontSize) + ((labels.length - 1) * (this._model.bodyFontSize / 2)) + (this._model.yPadding * 2) + this._model.titleFontSize * 1.5; + + // var titleWidth = ctx.measureText(this._model.title).width, + // //Label has a legend square as well so account for this. + // labelWidth = helpers.longestText(ctx, this.font, labels) + this._model.bodyFontSize + 3, + // longestTextWidth = helpers.max([labelWidth, titleWidth]); + + // this._model.width = longestTextWidth + (this._model.xPadding * 2); + + + // var halfHeight = this._model.height / 2; + + // //Check to ensure the height will fit on the canvas + // if (this._model.y - halfHeight < 0) { + // this._model.y = halfHeight; + // } else if (this._model.y + halfHeight > this._chart.height) { + // this._model.y = this._chart.height - halfHeight; + // } + + // //Decide whether to align left or right based on position on canvas + // if (this._model.x > this._chart.width / 2) { + // this._model.x -= this._model.xOffset + this._model.width; + // } else { + // this._model.x += this._model.xOffset; + // } + // break; + } + + // Build the Text Lines + helpers.extend(this._model, { + title: this.getTitle(xLabel, yLabel, element._index, element._datasetIndex, this._data), + body: this.getBody(xLabel, yLabel, element._index, element._datasetIndex, this._data), + footer: this.getFooter(xLabel, yLabel, element._index, element._datasetIndex, this._data), + }); + + helpers.extend(this._model, { + x: Math.round(tooltipPosition.x), + y: Math.round(tooltipPosition.y), + caretPadding: tooltipPosition.padding + }); + return this; }, draw: function() { @@ -211,137 +347,217 @@ var ctx = this._chart.ctx; var vm = this._view; - switch (this._options.hover.mode) { - case 'single': + // Get Dimensions - ctx.font = helpers.fontString(vm.fontSize, vm._fontStyle, vm._fontFamily); + vm.position = "top"; - vm.xAlign = "center"; - vm.yAlign = "above"; + var caretPadding = vm.caretPadding || 2; - //Distance between the actual element.y position and the start of the tooltip caret - var caretPadding = vm.caretPadding || 2; + // Height + var tooltipHeight = vm.yPadding * 2; - var tooltipWidth = ctx.measureText(vm.text).width + 2 * vm.xPadding, - tooltipRectHeight = vm.fontSize + 2 * vm.yPadding, - tooltipHeight = tooltipRectHeight + vm.caretHeight + caretPadding; + tooltipHeight += vm.title.length * vm.titleFontSize; // Line Height + tooltipHeight += vm.title.length ? vm.yPadding : 0; + tooltipHeight += vm.body.length * (vm.bodyFontSize); // Line Height + tooltipHeight += vm.footer.length ? vm.yPadding : 0; + tooltipHeight += vm.footer.length * (vm.footerFontSize); // Line Height - if (vm.x + tooltipWidth / 2 > this._chart.width) { - vm.xAlign = "left"; - } else if (vm.x - tooltipWidth / 2 < 0) { - vm.xAlign = "right"; - } + // Width + var tooltipWidth = 0; + helpers.each(vm.title, function(line, i) { + ctx.font = helpers.fontString(vm.titleFontSize, vm._titleFontStyle, vm._titleFontFamily); + tooltipWidth = Math.max(tooltipWidth, ctx.measureText(line).width); + }); + helpers.each(vm.body, function(line, i) { + ctx.font = helpers.fontString(vm.bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily); + tooltipWidth = Math.max(tooltipWidth, ctx.measureText(line).width); + }); + helpers.each(vm.footer, function(line, i) { + 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; - if (vm.y - tooltipHeight < 0) { - vm.yAlign = "below"; - } - var tooltipX = vm.x - tooltipWidth / 2, - tooltipY = vm.y - tooltipHeight; + // Smart Tooltip placement to stay on the canvas - ctx.fillStyle = helpers.color(vm.backgroundColor).alpha(vm.opacity).rgbString(); + // 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"; + } - // Custom Tooltips - if (this._options.tooltips.custom) { - this._options.tooltips.custom(this); - } - if (!this._options.tooltips.enabled) { - return; - } - switch (vm.yAlign) { - case "above": - //Draw a caret above the x/y - ctx.beginPath(); - ctx.moveTo(vm.x, vm.y - caretPadding); - ctx.lineTo(vm.x + vm.caretHeight, vm.y - (caretPadding + vm.caretHeight)); - ctx.lineTo(vm.x - vm.caretHeight, vm.y - (caretPadding + vm.caretHeight)); - ctx.closePath(); - ctx.fill(); - break; - case "below": - tooltipY = vm.y + caretPadding + vm.caretHeight; - //Draw a caret below the x/y - ctx.beginPath(); - ctx.moveTo(vm.x, vm.y + caretPadding); - ctx.lineTo(vm.x + vm.caretHeight, vm.y + caretPadding + vm.caretHeight); - ctx.lineTo(vm.x - vm.caretHeight, vm.y + caretPadding + vm.caretHeight); - ctx.closePath(); - ctx.fill(); - break; + // 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 + + if (this._options.tooltips.enabled) { + ctx.fillStyle = helpers.color(vm.backgroundColor).alpha(vm.opacity).rgbString(); + helpers.drawRoundedRectangle(ctx, tooltipX, tooltipY, tooltipWidth, tooltipHeight, vm.cornerRadius); + ctx.fill(); + } + + + // Draw Caret + if (this._options.tooltips.enabled) { + ctx.fillStyle = helpers.color(vm.backgroundColor).alpha(vm.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 bodyStart, + footerStart; + + // Titles + ctx.textAlign = vm._titleAlign; + ctx.textBaseline = "top"; + ctx.fillStyle = helpers.color(vm.titleColor).alpha(vm.opacity).rgbString(); + ctx.font = helpers.fontString(vm.titleFontSize, vm._titleFontStyle, vm._titleFontFamily); + + helpers.each(vm.title, function(title, i) { + var yPos = tooltipY + vm.yPadding + (vm.titleFontSize * i); + ctx.fillText(title, tooltipX + vm.xPadding, yPos); + if (i + 1 == vm.title.length) { + bodyStart = yPos + vm.yPadding + vm.titleFontSize; } + }, this); + + + // Body + ctx.textAlign = vm._bodyAlign; + ctx.textBaseline = "top"; + ctx.fillStyle = helpers.color(vm.bodyColor).alpha(vm.opacity).rgbString(); + ctx.font = helpers.fontString(vm.bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily); - switch (vm.xAlign) { - case "left": - tooltipX = vm.x - tooltipWidth + (vm.cornerRadius + vm.caretHeight); - break; - case "right": - tooltipX = vm.x - (vm.cornerRadius + vm.caretHeight); - break; + console.log(bodyStart); + + helpers.each(vm.body, function(body, i) { + var yPos = bodyStart + (vm.bodyFontSize * i); + ctx.fillText(body, tooltipX + vm.xPadding, yPos); + if (i + 1 == vm.body.length) { + footerStart = yPos + vm.bodyFontSize; } + }, this); + + // Footer + ctx.textAlign = vm._footerAlign; + ctx.textBaseline = "top"; + ctx.fillStyle = helpers.color(vm.footerColor).alpha(vm.opacity).rgbString(); + ctx.font = helpers.fontString(vm.footerFontSize, vm._footerFontStyle, vm._footerFontFamily); + + helpers.each(vm.footer, function(footer, i) { + var yPos = footerStart + vm.yPadding + (vm.footerFontSize * i); + ctx.fillText(footer, tooltipX + vm.xPadding, yPos); + }, this); + + } + + return; + + // Draw Body + ctx.font = helpers.fontString(vm.bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily); + + // Draw Footer + + // Custom Tooltips + + if (this._options.tooltips.custom) { + this._options.tooltips.custom(this); + } + + switch (this._options.tooltips.mode) { + case 'single': - helpers.drawRoundedRectangle(ctx, tooltipX, tooltipY, tooltipWidth, tooltipRectHeight, vm.cornerRadius); - ctx.fill(); ctx.fillStyle = helpers.color(vm.textColor).alpha(vm.opacity).rgbString(); ctx.textAlign = "center"; ctx.textBaseline = "middle"; - ctx.fillText(vm.text, tooltipX + tooltipWidth / 2, tooltipY + tooltipRectHeight / 2); + ctx.fillText(vm.text, tooltipX + tooltipWidth / 2, tooltipY + tooltipHeight / 2); break; case 'label': - // Custom Tooltips - if (this._options.tooltips.custom) { - this._options.tooltips.custom(this); - } - if (!this._options.tooltips.enabled) { - return; - } - helpers.drawRoundedRectangle(ctx, vm.x, vm.y - vm.height / 2, vm.width, vm.height, vm.cornerRadius); - ctx.fillStyle = helpers.color(vm.backgroundColor).alpha(vm.opacity).rgbString(); - ctx.fill(); - ctx.closePath(); + //helpers.drawRoundedRectangle(ctx, vm.x, vm.y - vm.height / 2, vm.width, vm.height, vm.cornerRadius); + // ctx.fillStyle = helpers.color(vm.backgroundColor).alpha(vm.opacity).rgbString(); + // ctx.fill(); + // ctx.closePath(); + // Title ctx.textAlign = "left"; ctx.textBaseline = "middle"; - ctx.fillStyle = helpers.color(vm.titleTextColor).alpha(vm.opacity).rgbString(); - ctx.font = helpers.fontString(vm.fontSize, vm._titleFontStyle, vm._titleFontFamily); + ctx.fillStyle = helpers.color(vm.titleColor).alpha(vm.opacity).rgbString(); + ctx.font = helpers.fontString(vm.bodyFontSize, vm._titleFontStyle, vm._titleFontFamily); ctx.fillText(vm.title, vm.x + vm.xPadding, this.getLineHeight(0)); - ctx.font = helpers.fontString(vm.fontSize, vm._fontStyle, vm._fontFamily); + ctx.font = helpers.fontString(vm.bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily); helpers.each(vm.labels, function(label, index) { ctx.fillStyle = helpers.color(vm.textColor).alpha(vm.opacity).rgbString(); - ctx.fillText(label, vm.x + vm.xPadding + vm.fontSize + 3, this.getLineHeight(index + 1)); + ctx.fillText(label, vm.x + vm.xPadding + vm.bodyFontSize + 3, this.getLineHeight(index + 1)); //A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas) - //ctx.clearRect(vm.x + vm.xPadding, this.getLineHeight(index + 1) - vm.fontSize/2, vm.fontSize, vm.fontSize); + //ctx.clearRect(vm.x + vm.xPadding, this.getLineHeight(index + 1) - vm.bodyFontSize/2, vm.bodyFontSize, vm.bodyFontSize); //Instead we'll make a white filled block to put the legendColour palette over. ctx.fillStyle = helpers.color(vm.legendColors[index].stroke).alpha(vm.opacity).rgbString(); - ctx.fillRect(vm.x + vm.xPadding - 1, this.getLineHeight(index + 1) - vm.fontSize / 2 - 1, vm.fontSize + 2, vm.fontSize + 2); + ctx.fillRect(vm.x + vm.xPadding - 1, this.getLineHeight(index + 1) - vm.bodyFontSize / 2 - 1, vm.bodyFontSize + 2, vm.bodyFontSize + 2); ctx.fillStyle = helpers.color(vm.legendColors[index].fill).alpha(vm.opacity).rgbString(); - ctx.fillRect(vm.x + vm.xPadding, this.getLineHeight(index + 1) - vm.fontSize / 2, vm.fontSize, vm.fontSize); + ctx.fillRect(vm.x + vm.xPadding, this.getLineHeight(index + 1) - vm.bodyFontSize / 2, vm.bodyFontSize, vm.bodyFontSize); }, this); break; } }, - getLineHeight: function(index) { - var baseLineHeight = this._view.y - (this._view.height / 2) + this._view.yPadding, - afterTitleIndex = index - 1; - - //If the index is zero, we're getting the title - if (index === 0) { - return baseLineHeight + this._view.titleFontSize / 2; - } else { - return baseLineHeight + ((this._view.fontSize * 1.5 * afterTitleIndex) + this._view.fontSize / 2) + this._view.titleFontSize * 1.5; - } - - }, }); }).call(this); diff --git a/src/scales/scale.radialLinear.js b/src/scales/scale.radialLinear.js index b9fae20a7..269d788de 100644 --- a/src/scales/scale.radialLinear.js +++ b/src/scales/scale.radialLinear.js @@ -59,7 +59,7 @@ this.height = this.maxHeight; this.xCenter = Math.round(this.width / 2); this.yCenter = Math.round(this.height / 2); - + var minSize = helpers.min([this.height, this.width]); this.drawingArea = (this.options.display) ? (minSize / 2) - (this.options.ticks.fontSize / 2 + this.options.ticks.backdropPaddingY) : (minSize / 2); }, @@ -201,9 +201,7 @@ for (i = 0; i < this.getValueCount(); i++) { // 5px to space the text slightly out - similar to what we do in the draw function. pointPosition = this.getPointPosition(i, largestPossibleRadius); - textWidth = this.ctx.measureText(helpers.template(this.options.ticks.template, { - value: this.data.labels[i] - })).width + 5; + textWidth = this.ctx.measureText(this.options.ticks.callback(this.data.labels[i])).width + 5; if (i === 0 || i === this.getValueCount() / 2) { // If we're at index zero, or exactly the middle, we're at exactly the top/bottom // of the radar chart, so text will be aligned centrally, so we'll half it and compare -- 2.47.3