From ef5cec7f5e7ea86e1ba72a2ad3b12079960e971a Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Tue, 6 Oct 2015 20:40:25 -0600 Subject: [PATCH 01/17] 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 9e0cf17d78d..546f69acd69 100644 --- a/samples/line.html +++ b/samples/line.html @@ -13,7 +13,7 @@ -
+

@@ -55,6 +55,35 @@

Legend

}, 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 6d8d5e25e2b..ccb3a7a0608 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 8c848deba9f..d2f6c4d253e 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 d930c024fd0..a4eb71294d7 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 40c73b4f551..295346a68e1 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 92e2b136463..38714968b7a 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 b9fae20a7b6..269d788de7f 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 From b835d5f209dfb8d5a7b772371236cadf0a1d3c27 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Mon, 12 Oct 2015 14:51:00 -0600 Subject: [PATCH 02/17] Tooltip Hooks --- samples/tooltip-hooks.html | 193 +++++++++++++++++++++++ src/core/core.scale.js | 2 +- src/core/core.tooltip.js | 314 ++++++++++++------------------------- 3 files changed, 294 insertions(+), 215 deletions(-) create mode 100644 samples/tooltip-hooks.html diff --git a/samples/tooltip-hooks.html b/samples/tooltip-hooks.html new file mode 100644 index 00000000000..1fbe8d20183 --- /dev/null +++ b/samples/tooltip-hooks.html @@ -0,0 +1,193 @@ + + + + + Line Chart + + + + + + +
+ +
+
+
+ + + + + +
+

Legend

+
+
+
+ + + + diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 295346a68e1..854c48892f2 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -348,7 +348,7 @@ return this.left + Math.round(valueOffset); } else { - return this.top + (decimal * (this.height / this.ticks.length)); + return this.top + (decimal * this.height); } }, diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 38714968b7a..2dc4dfd4511 100644 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -14,16 +14,21 @@ titleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", titleFontSize: 12, titleFontStyle: "bold", + titleSpacing: 2, + titleMarginBottom: 6, titleColor: "#fff", titleAlign: "left", bodyFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", bodyFontSize: 12, bodyFontStyle: "normal", + bodySpacing: 2, bodyColor: "#fff", bodyAlign: "left", footerFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", footerFontSize: 12, footerFontStyle: "bold", + footerSpacing: 2, + footerMarginTop: 6, footerColor: "#fff", footerAlign: "left", yPadding: 6, @@ -35,7 +40,7 @@ callbacks: { beforeTitle: helpers.noop, title: function(xLabel, yLabel, index, datasetIndex, data) { - return data.datasets[datasetIndex].label; + return this._options.tooltips.mode == 'single' ? data.datasets[datasetIndex].label : data.labels[index]; }, afterTitle: helpers.noop, @@ -70,6 +75,7 @@ _bodyFontFamily: options.tooltips.bodyFontFamily, _bodyFontStyle: options.tooltips.bodyFontStyle, bodyFontSize: options.tooltips.bodyFontSize, + bodySpacing: options.tooltips.bodySpacing, _bodposition: options.tooltips.bodposition, // Title @@ -78,6 +84,8 @@ _titleFontStyle: options.tooltips.titleFontStyle, titleFontSize: options.tooltips.titleFontSize, _titleAlign: options.tooltips.titleAlign, + titleSpacing: options.tooltips.titleSpacing, + titleMarginBottom: options.tooltips.titleMarginBottom, // Footer footerColor: options.tooltips.footerColor, @@ -85,6 +93,8 @@ _footerFontStyle: options.tooltips.footerFontStyle, footerFontSize: options.tooltips.footerFontSize, _footerAlign: options.tooltips.footerAlign, + footerSpacing: options.tooltips.footerSpacing, + footerMarginTop: options.tooltips.footerMarginTop, // Appearance caretSize: options.tooltips.caretSize, @@ -119,11 +129,6 @@ var lines = []; - var beforeBody = this._options.tooltips.callbacks.beforeBody.apply(this, arguments); - if (beforeBody) { - lines.push(beforeBody); - } - var beforeLabel, afterLabel, label; @@ -135,10 +140,10 @@ // 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); + beforeLabel = this._options.tooltips.callbacks.beforeLabel.call(this, xLabel[i], yLabel[i], index, datasetIndex); + afterLabel = this._options.tooltips.callbacks.afterLabel.call(this, xLabel[i], yLabel[i], index, datasetIndex); - labels.push((beforeLabel ? beforeLabel : '') + this._options.tooltips.callbacks.label(xLabel[i], yLabel[i], index, datasetIndex) + (afterLabel ? afterLabel : '')); + labels.push((beforeLabel ? beforeLabel : '') + this._options.tooltips.callbacks.label.call(this, xLabel[i], yLabel[i], index, datasetIndex) + (afterLabel ? afterLabel : '')); } @@ -159,11 +164,6 @@ } } - var afterBody = this._options.tooltips.callbacks.afterBody.apply(this, arguments); - if (afterBody) { - lines.push(afterBody); - } - return lines; }, @@ -194,6 +194,7 @@ var element = this._active[0], xLabel, yLabel, + labelColors = [], tooltipPosition; if (this._options.tooltips.mode == 'single') { @@ -206,138 +207,42 @@ xLabel = []; yLabel = []; + + console.log(this._active); + helpers.each(this._data.datasets, function(dataset, datasetIndex) { + xLabel.push(element._xScale.getLabelForIndex(element._index, datasetIndex)); yLabel.push(element._yScale.getLabelForIndex(element._index, datasetIndex)); }); + + helpers.each(this._active, function(active, i) { + labelColors.push({ + borderColor: active._view.borderColor, + backgroundColor: active._view.backgroundColor + }); + }, this); + tooltipPosition = this._active[0].tooltipPosition(); + tooltipPosition.y = this._active[0]._yScale.getPixelForDecimal(0.5); - // 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), + beforeBody: this._options.tooltips.callbacks.beforeBody.call(this, xLabel, yLabel, element._index, element._datasetIndex, this._data), body: this.getBody(xLabel, yLabel, element._index, element._datasetIndex, this._data), + afterBody: this._options.tooltips.callbacks.afterBody.call(this, 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 + caretPadding: tooltipPosition.padding, + labelColors: labelColors, }); return this; @@ -352,15 +257,21 @@ vm.position = "top"; var caretPadding = vm.caretPadding || 2; + var combinedBodyLength = vm.body.length + (vm.beforeBody ? 1 : 0) + (vm.afterBody ? 1 : 0); // Height - var tooltipHeight = vm.yPadding * 2; + 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 - 1) * vm.bodySpacing; // Body Line Spacing - 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 + tooltipHeight += vm.footer.length ? vm.footerMarginTop : 0; // Footer Margin + tooltipHeight += vm.footer.length * (vm.footerFontSize); // Footer Lines + tooltipHeight += (vm.footer.length - 1) * vm.footerSpacing; // Footer Line Spacing // Width var tooltipWidth = 0; @@ -370,8 +281,8 @@ }); 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); - }); + tooltipWidth = Math.max(tooltipWidth, ctx.measureText(line).width + (this._options.tooltips.mode != 'single' ? (vm.bodyFontSize + 2) : 0)); + }, this); 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); @@ -380,8 +291,8 @@ var tooltipTotalWidth = tooltipWidth + vm.caretSize + caretPadding; - // Smart Tooltip placement to stay on the canvas + // Smart Tooltip placement to stay on the canvas // Top, center, or bottom vm.yAlign = "center"; if (vm.y - (tooltipHeight / 2) < 0) { @@ -453,22 +364,25 @@ if (this._options.tooltips.enabled) { - var bodyStart, - footerStart; + var yBase = tooltipY + vm.yPadding; + var xBase = tooltipX + vm.xPadding; // 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); + if (vm.title.length) { + 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) { + 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); + } // Body @@ -477,85 +391,57 @@ ctx.fillStyle = helpers.color(vm.bodyColor).alpha(vm.opacity).rgbString(); ctx.font = helpers.fontString(vm.bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily); - console.log(bodyStart); + // Before Body + if (vm.beforeBody) { + ctx.fillText(vm.beforeBody, xBase, yBase); + yBase += vm.bodyFontSize + vm.bodySpacing; + } 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); + // Draw Legend-like boxes if needed + if (this._options.tooltips.mode != 'single') { + ctx.fillStyle = helpers.color(vm.labelColors[i].borderColor).alpha(vm.opacity).rgbString(); + ctx.fillRect(xBase, yBase, vm.bodyFontSize, vm.bodyFontSize); - 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); - } + ctx.fillStyle = helpers.color(vm.labelColors[i].backgroundColor).alpha(vm.opacity).rgbString(); + ctx.fillRect(xBase + 1, yBase + 1, vm.bodyFontSize - 2, vm.bodyFontSize - 2); - switch (this._options.tooltips.mode) { - case 'single': - - - - ctx.fillStyle = helpers.color(vm.textColor).alpha(vm.opacity).rgbString(); - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillText(vm.text, tooltipX + tooltipWidth / 2, tooltipY + tooltipHeight / 2); - break; - case 'label': + ctx.fillStyle = helpers.color(vm.bodyColor).alpha(vm.opacity).rgbString(); // Return fill style for text + } + // Body Line + ctx.fillText(body, xBase + (this._options.tooltips.mode != 'single' ? (vm.bodyFontSize + 2) : 0), yBase); - //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(); + yBase += vm.bodyFontSize + vm.bodySpacing; - // Title - ctx.textAlign = "left"; - ctx.textBaseline = "middle"; - 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)); + }, this); - 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.bodyFontSize + 3, this.getLineHeight(index + 1)); + // After Body + if (vm.afterBody) { + ctx.fillText(vm.afterBody, xBase, yBase); + yBase += vm.bodyFontSize; + } else { + yBase -= vm.bodySpacing; // Remove last body spacing + } - //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.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.bodyFontSize / 2 - 1, vm.bodyFontSize + 2, vm.bodyFontSize + 2); + // Footer + if (vm.footer.length) { - ctx.fillStyle = helpers.color(vm.legendColors[index].fill).alpha(vm.opacity).rgbString(); - ctx.fillRect(vm.x + vm.xPadding, this.getLineHeight(index + 1) - vm.bodyFontSize / 2, vm.bodyFontSize, vm.bodyFontSize); + yBase += vm.footerMarginTop; + 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) { + ctx.fillText(footer, xBase, yBase); + yBase += vm.footerFontSize + vm.footerSpacing; }, this); - break; + } + } }, }); From 19a7c57e2f19ca45cfc3ab207c06ad64a23c5880 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Sat, 17 Oct 2015 15:53:33 -0600 Subject: [PATCH 03/17] All tooltip callbacks support arrays for lines, and no drawing invisible tooltips. Simply return a string for a single line tooltip, or return an array to create multiple lines. --- samples/tooltip-hooks.html | 2 -- src/core/core.tooltip.js | 71 +++++++++++++++++++++++++++++--------- 2 files changed, 54 insertions(+), 19 deletions(-) diff --git a/samples/tooltip-hooks.html b/samples/tooltip-hooks.html index 1fbe8d20183..5b8ef3fa8aa 100644 --- a/samples/tooltip-hooks.html +++ b/samples/tooltip-hooks.html @@ -111,8 +111,6 @@

Legend

dataset.pointBorderWidth = 1; }); - console.log(config.data); - window.onload = function() { var ctx = document.getElementById("canvas").getContext("2d"); window.myLine = new Chart(ctx, config); diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 2dc4dfd4511..a1a94a1ee22 100644 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -114,17 +114,34 @@ var lines = []; if (beforeTitle) { - lines.push(beforeTitle); + if (helpers.isArray(beforeTitle)) { + lines = lines.concat(beforeTitle); + } else { + lines.push(beforeTitle); + } } if (title) { - lines.push(title); + if (helpers.isArray(title)) { + lines = lines.concat(title); + } else { + lines.push(title); + } } if (afterTitle) { - lines.push(afterTitle); + if (helpers.isArray(afterTitle)) { + lines = lines.concat(afterTitle); + } else { + lines.push(afterTitle); + } } return lines; }, + getBeforeBody: function(xLabel, yLabel, index, datasetIndex, data) { + var lines = this._options.tooltips.callbacks.beforeBody.call(this, xLabel, yLabel, index, datasetIndex, data); + return helpers.isArray(lines) ? lines : [lines]; + }, + getBody: function(xLabel, yLabel, index, datasetIndex) { var lines = []; @@ -167,6 +184,11 @@ return lines; }, + getAfterBody: function(xLabel, yLabel, index, datasetIndex, data) { + var lines = this._options.tooltips.callbacks.afterBody.call(this, xLabel, yLabel, index, datasetIndex, data); + return helpers.isArray(lines) ? lines : [lines]; + }, + getFooter: function() { var beforeFooter = this._options.tooltips.callbacks.beforeFooter.apply(this, arguments), footer = this._options.tooltips.callbacks.footer.apply(this, arguments), @@ -175,13 +197,25 @@ var lines = []; if (beforeFooter) { - lines.push(beforeFooter); + if (helpers.isArray(beforeFooter)) { + lines = lines.concat(beforeFooter); + } else { + lines.push(beforeFooter); + } } if (footer) { - lines.push(footer); + if (helpers.isArray(footer)) { + lines = lines.concat(footer); + } else { + lines.push(footer); + } } if (afterFooter) { - lines.push(afterFooter); + if (helpers.isArray(afterFooter)) { + lines = lines.concat(afterFooter); + } else { + lines.push(afterFooter); + } } return lines; @@ -208,8 +242,6 @@ xLabel = []; yLabel = []; - console.log(this._active); - helpers.each(this._data.datasets, function(dataset, datasetIndex) { xLabel.push(element._xScale.getLabelForIndex(element._index, datasetIndex)); @@ -232,9 +264,9 @@ // Build the Text Lines helpers.extend(this._model, { title: this.getTitle(xLabel, yLabel, element._index, element._datasetIndex, this._data), - beforeBody: this._options.tooltips.callbacks.beforeBody.call(this, xLabel, yLabel, element._index, element._datasetIndex, this._data), + beforeBody: this.getBeforeBody(xLabel, yLabel, element._index, element._datasetIndex, this._data), body: this.getBody(xLabel, yLabel, element._index, element._datasetIndex, this._data), - afterBody: this._options.tooltips.callbacks.afterBody.call(this, xLabel, yLabel, element._index, element._datasetIndex, this._data), + afterBody: this.getBeforeBody(xLabel, yLabel, element._index, element._datasetIndex, this._data), footer: this.getFooter(xLabel, yLabel, element._index, element._datasetIndex, this._data), }); @@ -252,12 +284,17 @@ var ctx = this._chart.ctx; var vm = this._view; + if (this._view.opacity === 0) { + return; + } + // Get Dimensions vm.position = "top"; var caretPadding = vm.caretPadding || 2; - var combinedBodyLength = vm.body.length + (vm.beforeBody ? 1 : 0) + (vm.afterBody ? 1 : 0); + + var combinedBodyLength = vm.body.length + vm.beforeBody.length + vm.afterBody.length; // Height var tooltipHeight = vm.yPadding * 2; // Tooltip Padding @@ -392,10 +429,10 @@ ctx.font = helpers.fontString(vm.bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily); // Before Body - if (vm.beforeBody) { + helpers.each(vm.beforeBody, function(beforeBody, i) { ctx.fillText(vm.beforeBody, xBase, yBase); yBase += vm.bodyFontSize + vm.bodySpacing; - } + }); helpers.each(vm.body, function(body, i) { @@ -418,12 +455,12 @@ }, this); // After Body - if (vm.afterBody) { + helpers.each(vm.afterBody, function(afterBody, i) { ctx.fillText(vm.afterBody, xBase, yBase); yBase += vm.bodyFontSize; - } else { - yBase -= vm.bodySpacing; // Remove last body spacing - } + }); + + yBase -= vm.bodySpacing; // Remove last body spacing // Footer From f650445ddccf01c6f0cf1233b042592ad1ed403b Mon Sep 17 00:00:00 2001 From: etimberg Date: Sun, 18 Oct 2015 10:39:54 -0400 Subject: [PATCH 04/17] Use correct function to get `afterBody` lines for tooltip. --- src/core/core.tooltip.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index a1a94a1ee22..9b227e18664 100644 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -266,7 +266,7 @@ title: this.getTitle(xLabel, yLabel, element._index, element._datasetIndex, this._data), beforeBody: this.getBeforeBody(xLabel, yLabel, element._index, element._datasetIndex, this._data), body: this.getBody(xLabel, yLabel, element._index, element._datasetIndex, this._data), - afterBody: this.getBeforeBody(xLabel, yLabel, element._index, element._datasetIndex, this._data), + afterBody: this.getAfterBody(xLabel, yLabel, element._index, element._datasetIndex, this._data), footer: this.getFooter(xLabel, yLabel, element._index, element._datasetIndex, this._data), }); From 180209e55c3ae3985c01cafffa5f7a51e4fb48d5 Mon Sep 17 00:00:00 2001 From: etimberg Date: Sun, 18 Oct 2015 16:13:57 -0400 Subject: [PATCH 05/17] Factor out `getLabelForIndex` into each scale so we can return appropriate data based on the scale type --- src/core/core.scale.js | 9 +++------ src/scales/scale.category.js | 4 ++++ src/scales/scale.linear.js | 4 ++++ src/scales/scale.logarithmic.js | 4 ++++ src/scales/scale.time.js | 4 ++++ 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 854c48892f2..a2e96eee912 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -313,12 +313,9 @@ 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 the value to display in the tooltip for the data at the given index + // function getLabelForIndex(index, datasetIndex) + getLabelForIndex: helpers.noop, // Used to get data value locations. Value can either be an index or a numerical value getPixelForValue: helpers.noop, diff --git a/src/scales/scale.category.js b/src/scales/scale.category.js index a2e569d4cfa..7c7656bcb56 100644 --- a/src/scales/scale.category.js +++ b/src/scales/scale.category.js @@ -15,6 +15,10 @@ this.ticks = this.data.labels; }, + getLabelForIndex: function(index, datasetIndex) { + return this.ticks[index]; + }, + // Used to get data value locations. Value can either be an index or a numerical value getPixelForValue: function(value, index, datasetIndex, includeOffset) { diff --git a/src/scales/scale.linear.js b/src/scales/scale.linear.js index d522ae91cf6..9b36820d8be 100644 --- a/src/scales/scale.linear.js +++ b/src/scales/scale.linear.js @@ -147,6 +147,10 @@ this.zeroLineIndex = this.ticks.indexOf(0); }, + getLabelForIndex: function(index, datasetIndex) { + return this.getRightValue(this.data.datasets[datasetIndex].data[index]); + }, + // Utils getPixelForValue: function(value, index, datasetIndex, includeOffset) { // This must be called after fit has been run so that diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 2db11bf2801..c8ce9bbbaf9 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -121,6 +121,10 @@ this.ticks = this.tickValues.slice(); }, + // Get the correct tooltip label + getLabelForIndex: function(index, datasetIndex) { + return this.getRightValue(this.data.datasets[datasetIndex].data[index]); + }, // Get the correct value. If the value type is object get the x or y based on whether we are horizontal or not getRightValue: function(rawValue) { return typeof rawValue === "object" ? (this.isHorizontal() ? rawValue.x : rawValue.y) : rawValue; diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 9ba486ce422..87b54ca8448 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -140,6 +140,10 @@ this.ticks.push(this.firstTick.clone().add(i, this.tickUnit)); } }, + // Get tooltip label + getLabelForIndex: function(index, datasetIndex) { + return this.data.labels[index]; + }, convertTicksToLabels: function() { this.ticks = this.ticks.map(function(tick, index, ticks) { var formattedTick = tick.format(this.options.time.displayFormat ? this.options.time.displayFormat : time.unit[this.tickUnit].display); From ceaa4ff03f7bc53bbc19711a023ea9313fdfde7f Mon Sep 17 00:00:00 2001 From: etimberg Date: Sun, 18 Oct 2015 16:31:18 -0400 Subject: [PATCH 06/17] Add a helper function to reduce code size --- src/core/core.tooltip.js | 70 ++++++++++++++-------------------------- 1 file changed, 24 insertions(+), 46 deletions(-) diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 9b227e18664..a45889d6122 100644 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -60,6 +60,19 @@ }, }; + // Helper to push or concat based on if the 2nd parameter is an array or not + function pushOrConcat(base, toPush) { + if (toPush) { + if (helpers.isArray(toPush)) { + base = base.concat(toPush); + } else { + base.push(toPush); + } + } + + return base; + } + Chart.Tooltip = Chart.Element.extend({ initialize: function() { var options = this._options; @@ -106,34 +119,17 @@ }); }, + // Get the title 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); var lines = []; + lines = pushOrConcat(lines, beforeTitle); + lines = pushOrConcat(lines, title); + lines = pushOrConcat(lines, afterTitle); - if (beforeTitle) { - if (helpers.isArray(beforeTitle)) { - lines = lines.concat(beforeTitle); - } else { - lines.push(beforeTitle); - } - } - if (title) { - if (helpers.isArray(title)) { - lines = lines.concat(title); - } else { - lines.push(title); - } - } - if (afterTitle) { - if (helpers.isArray(afterTitle)) { - lines = lines.concat(afterTitle); - } else { - lines.push(afterTitle); - } - } return lines; }, @@ -189,34 +185,16 @@ return helpers.isArray(lines) ? lines : [lines]; }, + // Get the footer and beforeFooter and afterFooter 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 beforeFooter = this._options.tooltips.callbacks.beforeFooter.apply(this, arguments); + var footer = this._options.tooltips.callbacks.footer.apply(this, arguments); + var afterFooter = this._options.tooltips.callbacks.afterFooter.apply(this, arguments); var lines = []; - - if (beforeFooter) { - if (helpers.isArray(beforeFooter)) { - lines = lines.concat(beforeFooter); - } else { - lines.push(beforeFooter); - } - } - if (footer) { - if (helpers.isArray(footer)) { - lines = lines.concat(footer); - } else { - lines.push(footer); - } - } - if (afterFooter) { - if (helpers.isArray(afterFooter)) { - lines = lines.concat(afterFooter); - } else { - lines.push(afterFooter); - } - } + lines = pushOrConcat(lines, beforeFooter); + lines = pushOrConcat(lines, footer); + lines = pushOrConcat(lines, afterFooter); return lines; }, From 7603b3cc36f6c5ac172855ff4ec722c4442bf738 Mon Sep 17 00:00:00 2001 From: etimberg Date: Sun, 18 Oct 2015 18:00:46 -0400 Subject: [PATCH 07/17] Time scales now support passing in data as points. Added this to the time scale sample file. --- samples/line-time-scale.html | 16 +++++++ src/core/core.scale.js | 9 ++-- src/scales/scale.category.js | 1 - src/scales/scale.linear.js | 6 --- src/scales/scale.logarithmic.js | 4 -- src/scales/scale.time.js | 80 +++++++++++++++++++++++++-------- 6 files changed, 82 insertions(+), 34 deletions(-) diff --git a/samples/line-time-scale.html b/samples/line-time-scale.html index 327054703e4..224fb392b95 100644 --- a/samples/line-time-scale.html +++ b/samples/line-time-scale.html @@ -63,6 +63,22 @@

Legend

}, { label: "My Second dataset", data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()], + }, { + label: "Dataset with point data", + data: [{ + x: "12/31/2014 06:00", + y: randomScalingFactor() + }, { + x: "01/04/2015 13:00", + y: randomScalingFactor() + }, { + x: "01/07/2015 01:15", + y: randomScalingFactor() + }, { + x: "01/15/2015 01:15", + y: randomScalingFactor() + }], + fill: false }] }, options: { diff --git a/src/core/core.scale.js b/src/core/core.scale.js index a2e96eee912..c9f8423399c 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -304,14 +304,15 @@ }, afterFit: helpers.noop, - - - - // Shared Methods isHorizontal: function() { return this.options.position == "top" || this.options.position == "bottom"; }, + + // Get the correct value. If the value type is object get the x or y based on whether we are horizontal or not + getRightValue: function(rawValue) { + return (typeof(rawValue) === "object" && rawValue !== null) ? (this.isHorizontal() ? rawValue.x : rawValue.y) : rawValue; + }, // Used to get the value to display in the tooltip for the data at the given index // function getLabelForIndex(index, datasetIndex) diff --git a/src/scales/scale.category.js b/src/scales/scale.category.js index 7c7656bcb56..e85ba04ea0c 100644 --- a/src/scales/scale.category.js +++ b/src/scales/scale.category.js @@ -21,7 +21,6 @@ // Used to get data value locations. Value can either be an index or a numerical value getPixelForValue: function(value, index, datasetIndex, includeOffset) { - if (this.isHorizontal()) { var innerWidth = this.width - (this.paddingLeft + this.paddingRight); var valueWidth = innerWidth / Math.max((this.data.labels.length - ((this.options.gridLines.offsetGridLines) ? 0 : 1)), 1); diff --git a/src/scales/scale.linear.js b/src/scales/scale.linear.js index 9b36820d8be..194e6da813c 100644 --- a/src/scales/scale.linear.js +++ b/src/scales/scale.linear.js @@ -169,12 +169,6 @@ return Math.round(pixel); } }, - - // Get the correct value. If the value type is object get the x or y based on whether we are horizontal or not - getRightValue: function(rawValue) { - return (typeof(rawValue) === "object" && rawValue !== null) ? (this.isHorizontal() ? rawValue.x : rawValue.y) : rawValue; - }, - }); Chart.scaleService.registerScaleType("linear", LinearScale, defaultConfig); diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index c8ce9bbbaf9..53ef000ce6b 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -125,10 +125,6 @@ getLabelForIndex: function(index, datasetIndex) { return this.getRightValue(this.data.datasets[datasetIndex].data[index]); }, - // Get the correct value. If the value type is object get the x or y based on whether we are horizontal or not - getRightValue: function(rawValue) { - return typeof rawValue === "object" ? (this.isHorizontal() ? rawValue.x : rawValue.y) : rawValue; - }, getPixelForTick: function(index, includeOffset) { return this.getPixelForValue(this.tickValues[index], null, null, includeOffset); }, diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 87b54ca8448..2ed211528b7 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -74,23 +74,59 @@ }; var TimeScale = Chart.Scale.extend({ - buildTicks: function(index) { - - this.ticks = []; - this.labelMoments = []; + getLabelMoment: function(datasetIndex, index) { + return this.labelMoments[datasetIndex][index]; + }, - // Parse each label into a moment - this.data.labels.forEach(function(label, index) { + buildLabelMoments: function() { + // Only parse these once. If the dataset does not have data as x,y pairs, we will use + // these + var scaleLabelMoments = []; + helpers.each(this.data.labels, function(label, index) { var labelMoment = this.parseTime(label); if (this.options.time.round) { labelMoment.startOf(this.options.time.round); } - this.labelMoments.push(labelMoment); + scaleLabelMoments.push(labelMoment); + }, this); + + this.firstTick = moment.min.call(this, scaleLabelMoments); + this.lastTick = moment.max.call(this, scaleLabelMoments); + + helpers.each(this.data.datasets, function(dataset, datasetIndex) { + var momentsForDataset = []; + + if (typeof dataset.data[0] === 'object') { + helpers.each(dataset.data, function(value, index) { + var labelMoment = this.parseTime(this.getRightValue(value)); + if (this.options.time.round) { + labelMoment.startOf(this.options.time.round); + } + momentsForDataset.push(labelMoment); + + // May have gone outside the scale ranges, make sure we keep the first and last ticks updated + this.firstTick = moment.min(this.firstTick, labelMoment); + this.lastTick = moment.max(this.lastTick, labelMoment); + }, this); + } else { + // We have no labels. Use the ones from the scale + momentsForDataset = scaleLabelMoments; + } + + this.labelMoments.push(momentsForDataset); }, this); - // Find the first and last moments, and range - this.firstTick = moment.min.call(this, this.labelMoments).clone(); - this.lastTick = moment.max.call(this, this.labelMoments).clone(); + // We will modify these, so clone for later + this.firstTick = this.firstTick.clone(); + this.lastTick = this.lastTick.clone(); + }, + + buildTicks: function(index) { + + this.ticks = []; + this.labelMoments = []; + + this.buildLabelMoments(); // Set unit override if applicable if (this.options.time.unit) { @@ -124,11 +160,11 @@ this.lastTick.endOf(this.tickUnit); this.smallestLabelSeparation = this.width; - var i = 0; - - for (i = 1; i < this.labelMoments.length; i++) { - this.smallestLabelSeparation = Math.min(this.smallestLabelSeparation, this.labelMoments[i].diff(this.labelMoments[i - 1], this.tickUnit, true)); - } + helpers.each(this.data.datasets, function(dataset, datasetIndex) { + for (var i = 1; i < this.labelMoments[datasetIndex].length; i++) { + this.smallestLabelSeparation = Math.min(this.smallestLabelSeparation, this.labelMoments[datasetIndex][i].diff(this.labelMoments[datasetIndex][i - 1], this.tickUnit, true)); + } + }, this); // Tick displayFormat override if (this.options.time.displayFormat) { @@ -136,13 +172,19 @@ } // For every unit in between the first and last moment, create a moment and add it to the ticks tick - for (i = 0; i <= this.tickRange; ++i) { + for (var i = 0; i <= this.tickRange; ++i) { this.ticks.push(this.firstTick.clone().add(i, this.tickUnit)); } }, // Get tooltip label getLabelForIndex: function(index, datasetIndex) { - return this.data.labels[index]; + var label = this.data.labels[index]; + + if (typeof this.data.datasets[datasetIndex].data[0] === 'object') { + label = this.getRightValue(this.data.datasets[datasetIndex].data[index]); + } + + return label; }, convertTicksToLabels: function() { this.ticks = this.ticks.map(function(tick, index, ticks) { @@ -156,8 +198,8 @@ }, this); }, getPixelForValue: function(value, index, datasetIndex, includeOffset) { - - var offset = this.labelMoments[index].diff(this.firstTick, this.tickUnit, true); + var labelMoment = this.getLabelMoment(datasetIndex, index); + var offset = labelMoment.diff(this.firstTick, this.tickUnit, true); var decimal = offset / this.tickRange; From 082391e3859f6941e11f962fb200d3284587a933 Mon Sep 17 00:00:00 2001 From: etimberg Date: Sun, 18 Oct 2015 18:14:56 -0400 Subject: [PATCH 08/17] Update docs a bit for new items. Small updates for tooltip labels. --- docs/00-Getting-Started.md | 62 +++++++++++++++++++++++++++++--------- src/core/core.tooltip.js | 5 +-- 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/docs/00-Getting-Started.md b/docs/00-Getting-Started.md index cbbb5ba29c5..39db54b3792 100644 --- a/docs/00-Getting-Started.md +++ b/docs/00-Getting-Started.md @@ -101,8 +101,22 @@ Chart.defaults.global = { // Element defaults defined in element extensions elements: {}, - // Legend template string - legendTemplate: "
    -legend\"><% for (var i = 0; i < data.datasets.length; i++){%>
  • \"><%if(data.datasets[i].label){%><%=data.datasets[i].label%><%}%>
  • <%}%>
", + // Legend callback function. + // @param {Chart} chart : the chart object to generate a legend for + legendCallback: legendCallback: function(chart) { + var text = []; + text.push('
    '); + for (var i = 0; i < chart.data.datasets.length; i++) { + text.push('
  • '); + if (chart.data.datasets[i].label) { + text.push(chart.data.datasets[i].label); + } + text.push('
  • '); + } + text.push('
'); + + return text.join(""); + } animation: { duration: 1000, @@ -128,18 +142,38 @@ Chart.defaults.global = { caretSize: 8, cornerRadius: 6, xOffset: 10, - template: [ - '<% if(label){ %>', - '<%=label %>: ', - '<% } %>', - '<%=value %>', - ].join(''), - multiTemplate: [ - '<%if (datasetLabel){ %>', - '<%=datasetLabel %>: ', - '<% } %>', - '<%=value %>' - ].join(''), + // V2.0 introduces callback functions as a replacement for the template engine in v1. The tooltip + // has the following callbacks for providing text. For all functions, 'this' will be the tooltip object + // create from the Chart.Tooltip constructor + // + // All functions are called with the same arguments + // - xLabel : string or array of strings. This is the xDataValue for each item to be displayed in the tooltip + // - yLabel : string or array of strings. This is the yDataValue for each item to be displayed in the tooltip + // - index : number. Data index + // - datasetIndex : number. Dataset index + // - data : object. Data object passed to chart + callbacks: { + beforeTitle: helpers.noop, + title: function(xLabel, yLabel, index, datasetIndex, data) { + // If there are multiple items, use the xLabel of the + return helpers.isArray(xLabel) ? xLabel[0] : xLabel; + }, + afterTitle: helpers.noop, + + beforeBody: helpers.noop, + + beforeLabel: helpers.noop, + label: function(xLabel, yLabel, index, datasetIndex, data) { + return this._data.datasets[datasetIndex].label + ': ' + yLabel; + }, + afterLabel: helpers.noop, + + afterBody: helpers.noop, + + beforeFooter: helpers.noop, + footer: helpers.noop, + afterFooter: helpers.noop, + }, multiKeyBackground: '#fff', }, diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index a45889d6122..29c19488aa9 100644 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -40,7 +40,8 @@ callbacks: { beforeTitle: helpers.noop, title: function(xLabel, yLabel, index, datasetIndex, data) { - return this._options.tooltips.mode == 'single' ? data.datasets[datasetIndex].label : data.labels[index]; + // Pick first label for now + return helpers.isArray(xLabel) ? xLabel[0] : xLabel; }, afterTitle: helpers.noop, @@ -48,7 +49,7 @@ beforeLabel: helpers.noop, label: function(xLabel, yLabel, index, datasetIndex, data) { - return xLabel + ': ' + yLabel; + return this._data.datasets[datasetIndex].label + ': ' + yLabel; }, afterLabel: helpers.noop, From 86a52d030233944cdedc14bc1c5076c74826ea87 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 18 Oct 2015 19:34:56 -0400 Subject: [PATCH 09/17] Fix some test failures --- src/core/core.scale.js | 2 +- src/scales/scale.logarithmic.js | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/core/core.scale.js b/src/core/core.scale.js index e768726ce74..556ea0039fe 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -48,7 +48,7 @@ reverse: false, show: true, callback: function(value) { - return value; + return '' + value; }, }, }; diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 59c3dd44f28..9f5b62c47f6 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -10,7 +10,15 @@ // label settings ticks: { - template: "<%var remain = value / (Math.pow(10, Math.floor(Chart.helpers.log10(value))));if (remain === 1 || remain === 2 || remain === 5) {%><%=value.toExponential()%><%} else {%><%= null %><%}%>", + callback: function(value) { + var remain = value / (Math.pow(10, Math.floor(Chart.helpers.log10(value)))); + + if (remain === 1 || remain === 2 || remain === 5) { + return value.toExponential() + } else { + return ''; + } + } } }; From d0475b8052ec0c9213d579c702c387d0c515b859 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 18 Oct 2015 19:48:57 -0400 Subject: [PATCH 10/17] Down to 3 test failures --- test/core.helpers.tests.js | 8 ++++++-- test/scale.category.tests.js | 5 ++++- test/scale.linear.tests.js | 4 +++- test/scale.logarithmic.tests.js | 5 ++++- test/scale.radialLinear.tests.js | 5 ++++- test/scale.time.tests.js | 5 ++++- 6 files changed, 25 insertions(+), 7 deletions(-) diff --git a/test/core.helpers.tests.js b/test/core.helpers.tests.js index ce0c987579f..0320a84aa57 100644 --- a/test/core.helpers.tests.js +++ b/test/core.helpers.tests.js @@ -244,7 +244,7 @@ describe('Core helper tests', function() { padding: 10, reverse: false, show: true, - template: "<%=value%>" + callback: merged.scales.yAxes[1].ticks.callback, // make it nicer, then check explicitly below }, type: 'linear' }, { @@ -281,12 +281,16 @@ describe('Core helper tests', function() { padding: 10, reverse: false, show: true, - template: "<%=value%>" + callback: merged.scales.yAxes[2].ticks.callback, // make it nicer, then check explicitly below }, type: 'linear' }] } }); + + // Are these actually functions + expect(merged.scales.yAxes[1].ticks.callback).toEqual(jasmine.any(Function)); + expect(merged.scales.yAxes[2].ticks.callback).toEqual(jasmine.any(Function)); }); it('should get value or default', function() { diff --git a/test/scale.category.tests.js b/test/scale.category.tests.js index d6ec96f960a..e5ac45a297c 100644 --- a/test/scale.category.tests.js +++ b/test/scale.category.tests.js @@ -43,9 +43,12 @@ describe('Category scale tests', function() { padding: 10, reverse: false, show: true, - template: "<%=value%>" + callback: defaultConfig.ticks.callback, // make this nicer, then check explicitly below } }); + + // Is this actually a function + expect(defaultConfig.ticks.callback).toEqual(jasmine.any(Function)); }); it('Should generate ticks from the data labales', function() { diff --git a/test/scale.linear.tests.js b/test/scale.linear.tests.js index dd31c7bce33..d0378ed2437 100644 --- a/test/scale.linear.tests.js +++ b/test/scale.linear.tests.js @@ -42,9 +42,11 @@ describe('Linear Scale', function() { padding: 10, reverse: false, show: true, - template: "<%=value%>" + callback: defaultConfig.ticks.callback, // make this work nicer, then check below } }); + + expect(defaultConfig.ticks.callback).toEqual(jasmine.any(Function)); }); it('Should correctly determine the max & min data values', function() { diff --git a/test/scale.logarithmic.tests.js b/test/scale.logarithmic.tests.js index ea1092749be..72250df73ba 100644 --- a/test/scale.logarithmic.tests.js +++ b/test/scale.logarithmic.tests.js @@ -41,9 +41,12 @@ describe('Logarithmic Scale tests', function() { padding: 10, reverse: false, show: true, - template: "<%var remain = value / (Math.pow(10, Math.floor(Chart.helpers.log10(value))));if (remain === 1 || remain === 2 || remain === 5) {%><%=value.toExponential()%><%} else {%><%= null %><%}%>", + callback: defaultConfig.ticks.callback, // make this nicer, then check explicitly below }, }); + + // Is this actually a function + expect(defaultConfig.ticks.callback).toEqual(jasmine.any(Function)); }); it('Should correctly determine the max & min data values', function() { diff --git a/test/scale.radialLinear.tests.js b/test/scale.radialLinear.tests.js index 5a762078918..0b2512e9b3b 100644 --- a/test/scale.radialLinear.tests.js +++ b/test/scale.radialLinear.tests.js @@ -58,10 +58,13 @@ describe('Test the radial linear scale', function() { reverse: false, showLabelBackdrop: true, show: true, - template: "<%=value%>", + callback: defaultConfig.ticks.callback, // make this nicer, then check explicitly below }, }); + + // Is this actually a function + expect(defaultConfig.ticks.callback).toEqual(jasmine.any(Function)); }); it('Should correctly determine the max & min data values', function() { diff --git a/test/scale.time.tests.js b/test/scale.time.tests.js index eb573df4a27..1fbd9ab8809 100644 --- a/test/scale.time.tests.js +++ b/test/scale.time.tests.js @@ -46,7 +46,7 @@ describe('Time scale tests', function() { padding: 10, reverse: false, show: true, - template: "<%=value%>" + callback: defaultConfig.ticks.callback, // make this nicer, then check explicitly below }, time: { format: false, @@ -55,6 +55,9 @@ describe('Time scale tests', function() { displayFormat: false, } }); + + // Is this actually a function + expect(defaultConfig.ticks.callback).toEqual(jasmine.any(Function)); }); it('should build ticks using days', function() { From 5495a34d02cd220ed040d2a6e0288f3d11ec5670 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sun, 18 Oct 2015 19:57:06 -0400 Subject: [PATCH 11/17] Fix 2 fails due to a bug fix for the scale label colour. --- test/scale.linear.tests.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/scale.linear.tests.js b/test/scale.linear.tests.js index d0378ed2437..ac190a3c5f9 100644 --- a/test/scale.linear.tests.js +++ b/test/scale.linear.tests.js @@ -841,6 +841,9 @@ describe('Linear Scale', function() { }, { "name": "stroke", "args": [] + }, { + "name": "setFillStyle", + "args": ["#666"] }, { "name": "fillText", "args": ["myLabel", 100, 122] @@ -1393,6 +1396,9 @@ describe('Linear Scale', function() { }, { "name": "rotate", "args": [-1.5707963267948966] + }, { + "name": "setFillStyle", + "args": ["#666"] }, { "name": "fillText", "args": ["", 0, 0] From bfabac945ffb1a0a2dddf8673a1775a9b8d478e2 Mon Sep 17 00:00:00 2001 From: etimberg Date: Mon, 19 Oct 2015 19:20:08 -0400 Subject: [PATCH 12/17] Labels are now optional for the time scale. --- samples/line-time-point-data.html | 164 ++++++++++++++++++++++++++++++ src/scales/scale.time.js | 29 +++--- 2 files changed, 181 insertions(+), 12 deletions(-) create mode 100644 samples/line-time-point-data.html diff --git a/samples/line-time-point-data.html b/samples/line-time-point-data.html new file mode 100644 index 00000000000..d8201202478 --- /dev/null +++ b/samples/line-time-point-data.html @@ -0,0 +1,164 @@ + + + + + Time Scale Point Data + + + + + + + +
+ +
+
+
+ + + +
+

Legend

+
+
+
+ + + + diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 2ed211528b7..ad1365dda74 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -82,16 +82,21 @@ // Only parse these once. If the dataset does not have data as x,y pairs, we will use // these var scaleLabelMoments = []; - helpers.each(this.data.labels, function(label, index) { - var labelMoment = this.parseTime(label); - if (this.options.time.round) { - labelMoment.startOf(this.options.time.round); - } - scaleLabelMoments.push(labelMoment); - }, this); + if (this.data.labels && this.data.labels.length > 0) { + helpers.each(this.data.labels, function(label, index) { + var labelMoment = this.parseTime(label); + if (this.options.time.round) { + labelMoment.startOf(this.options.time.round); + } + scaleLabelMoments.push(labelMoment); + }, this); - this.firstTick = moment.min.call(this, scaleLabelMoments); - this.lastTick = moment.max.call(this, scaleLabelMoments); + this.firstTick = moment.min.call(this, scaleLabelMoments); + this.lastTick = moment.max.call(this, scaleLabelMoments); + } else { + this.firstTick = null; + this.lastTick = null; + } helpers.each(this.data.datasets, function(dataset, datasetIndex) { var momentsForDataset = []; @@ -105,8 +110,8 @@ momentsForDataset.push(labelMoment); // May have gone outside the scale ranges, make sure we keep the first and last ticks updated - this.firstTick = moment.min(this.firstTick, labelMoment); - this.lastTick = moment.max(this.lastTick, labelMoment); + this.firstTick = this.firstTick !== null ? moment.min(this.firstTick, labelMoment) : labelMoment; + this.lastTick = this.lastTick !== null ? moment.max(this.lastTick, labelMoment) : labelMoment; }, this); } else { // We have no labels. Use the ones from the scale @@ -178,7 +183,7 @@ }, // Get tooltip label getLabelForIndex: function(index, datasetIndex) { - var label = this.data.labels[index]; + var label = this.data.labels && index < this.data.labels.length ? this.data.labels[index] : ''; if (typeof this.data.datasets[datasetIndex].data[0] === 'object') { label = this.getRightValue(this.data.datasets[datasetIndex].data[index]); From 35011e5ae396d5dd68961d14ec50d8ef5f3a809e Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Mon, 19 Oct 2015 21:23:01 -0600 Subject: [PATCH 13/17] Support min and max dates on time scale --- src/scales/scale.time.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index ad1365dda74..88af1436bf4 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -91,8 +91,17 @@ scaleLabelMoments.push(labelMoment); }, this); - this.firstTick = moment.min.call(this, scaleLabelMoments); - this.lastTick = moment.max.call(this, scaleLabelMoments); + if (this.options.time.min) { + this.firstTick = this.parseTime(this.options.time.min); + } else { + this.firstTick = moment.min.call(this, scaleLabelMoments); + } + + if (this.options.time.max) { + this.lastTick = this.parseTime(this.options.time.max); + } else { + this.lastTick = moment.max.call(this, scaleLabelMoments); + } } else { this.firstTick = null; this.lastTick = null; From 854c1af45e819aaa666401ef2bd2b2c3217c9800 Mon Sep 17 00:00:00 2001 From: etimberg Date: Wed, 21 Oct 2015 20:47:22 -0400 Subject: [PATCH 14/17] test fix --- test/scale.linear.tests.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/scale.linear.tests.js b/test/scale.linear.tests.js index 87b82662d52..d4a29eec3da 100644 --- a/test/scale.linear.tests.js +++ b/test/scale.linear.tests.js @@ -1399,9 +1399,6 @@ describe('Linear Scale', function() { }, { "name": "setFillStyle", "args": ["#666"] - }, { - "name": "setFillStyle", - "args": ["#666"] }, { "name": "fillText", "args": ["", 0, 0] From 965d74e34ab7151672f232c94c5515ce7e1d0152 Mon Sep 17 00:00:00 2001 From: etimberg Date: Wed, 21 Oct 2015 21:00:13 -0400 Subject: [PATCH 15/17] Time scale test fix for optional labels --- test/scale.time.tests.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/scale.time.tests.js b/test/scale.time.tests.js index 1fbd9ab8809..0e8771e99c3 100644 --- a/test/scale.time.tests.js +++ b/test/scale.time.tests.js @@ -135,6 +135,9 @@ describe('Time scale tests', function() { var mockData = { labels: ["2015-01-01T20:00:00", "2015-01-02T21:00:00", "2015-01-03T22:00:00", "2015-01-05T23:00:00", "2015-01-07T03:00", "2015-01-08T10:00", "2015-01-10T12:00"], // days + datasets: [{ + data: [], + }] }; var mockContext = window.createMockContext(); From bc41909e7ade0f001b0252a6608aee5b93f9f4e9 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Fri, 23 Oct 2015 12:40:38 -0600 Subject: [PATCH 16/17] Track lastTooltipActive for change animations Since we split up the hover and tooltips modes, both changes need to be tracked for visual updates between the two. --- src/core/core.controller.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 45e8869e5d1..13695081a09 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -371,6 +371,7 @@ }, eventHandler: function eventHandler(e) { this.lastActive = this.lastActive || []; + this.lastTooltipActive = this.lastTooltipActive || []; // Find Active Elements for hover and tooltips if (e.type == 'mouseout') { @@ -484,10 +485,19 @@ } }, this); + helpers.each(this.tooltipActive, function(element, index) { + if (element !== this.lastTooltipActive[index]) { + changed = true; + } + }, this); + // If entering, leaving, or changing elements, animate the change via pivot if ((!this.lastActive.length && this.active.length) || (this.lastActive.length && !this.active.length) || - (this.lastActive.length && this.active.length && changed)) { + (this.lastActive.length && this.active.length && changed) || + (!this.lastTooltipActive.length && this.tooltipActive.length) || + (this.lastTooltipActive.length && !this.tooltipActive.length) || + (this.lastTooltipActive.length && this.tooltipActive.length && changed)) { this.stop(); @@ -497,8 +507,9 @@ } } - // Remember Last Active + // Remember Last Actives this.lastActive = this.active; + this.lastTooltipActive = this.tooltipActive; return this; }, }); From 60f24267747e44fad6efa6c0e908ba4ecf4add30 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Fri, 23 Oct 2015 12:40:56 -0600 Subject: [PATCH 17/17] Only use active datasets in the tooltip body --- src/core/core.tooltip.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 29c19488aa9..35760763710 100644 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -222,7 +222,9 @@ yLabel = []; helpers.each(this._data.datasets, function(dataset, datasetIndex) { - + if (!helpers.isDatasetVisible(dataset)) { + return; + } xLabel.push(element._xScale.getLabelForIndex(element._index, datasetIndex)); yLabel.push(element._yScale.getLabelForIndex(element._index, datasetIndex)); }); @@ -415,6 +417,7 @@ helpers.each(vm.body, function(body, i) { + // Draw Legend-like boxes if needed if (this._options.tooltips.mode != 'single') { ctx.fillStyle = helpers.color(vm.labelColors[i].borderColor).alpha(vm.opacity).rgbString();