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 callback function. + // @param {Chart} chart : the chart object to generate a legend for + legendCallback: legendCallback: function(chart) { + var text = []; + 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/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/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/samples/line.html b/samples/line.html index 1179e6bcc63..e83f488fa42 100644 --- a/samples/line.html +++ b/samples/line.html @@ -13,7 +13,7 @@ -
+

@@ -59,6 +59,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/samples/tooltip-hooks.html b/samples/tooltip-hooks.html new file mode 100644 index 00000000000..5b8ef3fa8aa --- /dev/null +++ b/samples/tooltip-hooks.html @@ -0,0 +1,191 @@ + + + + + Line Chart + + + + + + +
+ +
+
+
+ + + + + +
+

Legend

+
+
+
+ + + + diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 8cb9bcd2d48..13695081a09 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -327,7 +327,7 @@ }, generateLegend: function generateLegend() { - return helpers.template(this.options.legendTemplate, this); + return this.options.legendCallback(this); }, destroy: function destroy() { @@ -371,10 +371,11 @@ }, eventHandler: function eventHandler(e) { this.lastActive = this.lastActive || []; + this.lastTooltipActive = this.lastTooltipActive || []; - // 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) { @@ -388,6 +389,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 @@ -403,6 +416,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) { @@ -445,11 +459,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(); @@ -471,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(); @@ -484,8 +507,9 @@ } } - // Remember Last Active + // Remember Last Actives this.lastActive = this.active; + this.lastTooltipActive = this.tooltipActive; return this; }, }); diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index ca27c7c3c85..9e1272ce290 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -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 = { diff --git a/src/core/core.js b/src/core/core.js index fb84f0e7304..922a845f104 100755 --- a/src/core/core.js +++ b/src/core/core.js @@ -91,8 +91,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 3a1373efa92..1338011500c 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; + }, }, }; @@ -133,14 +135,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, @@ -242,13 +242,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 @@ -315,6 +315,15 @@ 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) + getLabelForIndex: helpers.noop, // Used to get data value locations. Value can either be an index or a numerical value getPixelForValue: helpers.noop, @@ -344,7 +353,7 @@ return this.left + Math.round(valueOffset); } else { - return this.top + (decimal * (this.height / this.ticks.length)); + return this.top + (decimal * this.height); } }, @@ -503,7 +512,7 @@ } } - + this.ctx.translate(xLabelValue, yLabelValue); this.ctx.rotate(helpers.toRadians(this.labelRotation) * -1); this.ctx.font = labelFont; diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 92e2b136463..35760763710 100644 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -9,35 +9,71 @@ 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", + 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, 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) { + // Pick first label for now + 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, + }, }; + // 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; @@ -48,20 +84,34 @@ 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, + bodySpacing: options.tooltips.bodySpacing, + _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, + titleSpacing: options.tooltips.titleSpacing, + titleMarginBottom: options.tooltips.titleMarginBottom, + + // Footer + footerColor: options.tooltips.footerColor, + _footerFontFamily: options.tooltips.footerFontFamily, + _footerFontStyle: options.tooltips.footerFontStyle, + footerFontSize: options.tooltips.footerFontSize, + _footerAlign: options.tooltips.footerAlign, + footerSpacing: options.tooltips.footerSpacing, + footerMarginTop: options.tooltips.footerMarginTop, // Appearance - caretHeight: options.tooltips.caretSize, + caretSize: options.tooltips.caretSize, cornerRadius: options.tooltips.cornerRadius, backgroundColor: options.tooltips.backgroundColor, opacity: 0, @@ -69,141 +119,145 @@ }, }); }, - update: function() { - var ctx = this._chart.ctx; + // 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); - 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 lines = []; + lines = pushOrConcat(lines, beforeTitle); + lines = pushOrConcat(lines, title); + lines = pushOrConcat(lines, afterTitle); - var tooltipPosition = this._active[0].tooltipPosition(); - helpers.extend(this._model, { - x: Math.round(tooltipPosition.x), - y: Math.round(tooltipPosition.y), - caretPadding: tooltipPosition.padding - }); + return lines; + }, - break; + 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]; + }, - case 'label': + getBody: function(xLabel, yLabel, index, datasetIndex) { - // Tooltip Content + var lines = []; - var dataArray, - dataIndex; + var beforeLabel, + afterLabel, + label; - var labels = [], - colors = []; + if (helpers.isArray(xLabel)) { - 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 labels = []; - 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, - }); + // Run EACH label pair through the label callback this time. + for (var i = 0; i < xLabel.length; i++) { + 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); - // Calculate Appearance Tweaks + labels.push((beforeLabel ? beforeLabel : '') + this._options.tooltips.callbacks.label.call(this, xLabel[i], yLabel[i], index, datasetIndex) + (afterLabel ? afterLabel : '')); - 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]); + if (labels.length) { + lines = lines.concat(labels); + } - this._model.width = longestTextWidth + (this._model.xPadding * 2); + } else { + // Run the single label through the callback - var halfHeight = this._model.height / 2; + 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); - //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; - } + if (beforeLabel || label || afterLabel) { + lines.push((beforeLabel ? afterLabel : '') + label + (afterLabel ? afterLabel : '')); + } + } + + 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]; + }, + + // Get the footer and beforeFooter and afterFooter lines + getFooter: function() { + 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 = []; + lines = pushOrConcat(lines, beforeFooter); + lines = pushOrConcat(lines, footer); + lines = pushOrConcat(lines, afterFooter); + + return lines; + }, - //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; + update: function() { + + var ctx = this._chart.ctx; + + var element = this._active[0], + xLabel, + yLabel, + labelColors = [], + 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) { + if (!helpers.isDatasetVisible(dataset)) { + return; } - break; + 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); + } + + // Build the Text Lines + helpers.extend(this._model, { + 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.getAfterBody(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, + labelColors: labelColors, + }); + return this; }, draw: function() { @@ -211,136 +265,203 @@ var ctx = this._chart.ctx; var vm = this._view; - switch (this._options.hover.mode) { - case 'single': + if (this._view.opacity === 0) { + return; + } - ctx.font = helpers.fontString(vm.fontSize, vm._fontStyle, vm._fontFamily); + // Get Dimensions - vm.xAlign = "center"; - vm.yAlign = "above"; + vm.position = "top"; - //Distance between the actual element.y position and the start of the tooltip caret - var caretPadding = vm.caretPadding || 2; + var caretPadding = vm.caretPadding || 2; - var tooltipWidth = ctx.measureText(vm.text).width + 2 * vm.xPadding, - tooltipRectHeight = vm.fontSize + 2 * vm.yPadding, - tooltipHeight = tooltipRectHeight + vm.caretHeight + caretPadding; + var combinedBodyLength = vm.body.length + vm.beforeBody.length + vm.afterBody.length; - if (vm.x + tooltipWidth / 2 > this._chart.width) { - vm.xAlign = "left"; - } else if (vm.x - tooltipWidth / 2 < 0) { - vm.xAlign = "right"; - } + // Height + var tooltipHeight = vm.yPadding * 2; // Tooltip Padding - if (vm.y - tooltipHeight < 0) { - vm.yAlign = "below"; - } + 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 - var tooltipX = vm.x - tooltipWidth / 2, - tooltipY = vm.y - tooltipHeight; + tooltipHeight += combinedBodyLength * vm.bodyFontSize; // Body Lines + tooltipHeight += (combinedBodyLength - 1) * vm.bodySpacing; // Body Line Spacing - ctx.fillStyle = helpers.color(vm.backgroundColor).alpha(vm.opacity).rgbString(); + 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 - // Custom Tooltips - if (this._options.tooltips.custom) { - this._options.tooltips.custom(this); - } - if (!this._options.tooltips.enabled) { - return; - } + // 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 + (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); + }); + tooltipWidth += 2 * vm.xPadding; + var tooltipTotalWidth = tooltipWidth + vm.caretSize + caretPadding; - 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; - } - switch (vm.xAlign) { - case "left": - tooltipX = vm.x - tooltipWidth + (vm.cornerRadius + vm.caretHeight); - break; - case "right": - tooltipX = vm.x - (vm.cornerRadius + vm.caretHeight); - break; - } - helpers.drawRoundedRectangle(ctx, tooltipX, tooltipY, tooltipWidth, tooltipRectHeight, vm.cornerRadius); + // Smart Tooltip placement to stay on the canvas + // Top, center, or bottom + vm.yAlign = "center"; + if (vm.y - (tooltipHeight / 2) < 0) { + vm.yAlign = "top"; + } else if (vm.y + (tooltipHeight / 2) > this._chart.height) { + vm.yAlign = "bottom"; + } - 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); - break; - case 'label': + // Left or Right + vm.xAlign = "right"; + if (vm.x + tooltipTotalWidth > this._chart.width) { + vm.xAlign = "left"; + } - // 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(); + // 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 yBase = tooltipY + vm.yPadding; + var xBase = tooltipX + vm.xPadding; - 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.fillText(vm.title, vm.x + vm.xPadding, this.getLineHeight(0)); + // Titles - ctx.font = helpers.fontString(vm.fontSize, vm._fontStyle, vm._fontFamily); - 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)); + 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); + } - //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); - //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); + // 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); - 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); + // Before Body + 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) { + + + // 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); + + ctx.fillStyle = helpers.color(vm.labelColors[i].backgroundColor).alpha(vm.opacity).rgbString(); + ctx.fillRect(xBase + 1, yBase + 1, vm.bodyFontSize - 2, vm.bodyFontSize - 2); + + 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); + yBase += vm.bodyFontSize + vm.bodySpacing; + + }, this); + + // After Body + helpers.each(vm.afterBody, function(afterBody, i) { + ctx.fillText(vm.afterBody, xBase, yBase); + yBase += vm.bodyFontSize; + }); + + yBase -= vm.bodySpacing; // Remove last body spacing + + + // Footer + if (vm.footer.length) { + + 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; - } - }, - 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; } - }, }); diff --git a/src/scales/scale.category.js b/src/scales/scale.category.js index 7e572b864c3..039a9bef674 100644 --- a/src/scales/scale.category.js +++ b/src/scales/scale.category.js @@ -15,9 +15,12 @@ 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) { - 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 79ea5937f20..9de4d81cf6c 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 @@ -165,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 eba0877bae4..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 ''; + } + } } }; @@ -121,9 +129,9 @@ this.ticks = this.tickValues.slice(); }, - // 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; + // Get the correct tooltip label + getLabelForIndex: function(index, datasetIndex) { + return this.getRightValue(this.data.datasets[datasetIndex].data[index]); }, getPixelForTick: function(index, includeOffset) { return this.getPixelForValue(this.tickValues[index], null, null, includeOffset); diff --git a/src/scales/scale.radialLinear.js b/src/scales/scale.radialLinear.js index f55b321e348..d094eb4c89d 100644 --- a/src/scales/scale.radialLinear.js +++ b/src/scales/scale.radialLinear.js @@ -203,9 +203,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 diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index 9ba486ce422..88af1436bf4 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -74,23 +74,73 @@ }; var TimeScale = Chart.Scale.extend({ - buildTicks: function(index) { + getLabelMoment: function(datasetIndex, index) { + return this.labelMoments[datasetIndex][index]; + }, - this.ticks = []; - this.labelMoments = []; + buildLabelMoments: function() { + // Only parse these once. If the dataset does not have data as x,y pairs, we will use + // these + var scaleLabelMoments = []; + 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); + + 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; + } - // Parse each label into a moment - this.data.labels.forEach(function(label, index) { - var labelMoment = this.parseTime(label); - if (this.options.time.round) { - labelMoment.startOf(this.options.time.round); + 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 = 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 + momentsForDataset = scaleLabelMoments; } - this.labelMoments.push(labelMoment); + + 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 +174,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,10 +186,20 @@ } // 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) { + 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]); + } + + return label; + }, 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); @@ -152,8 +212,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; 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 cd7a2c4b574..d4a29eec3da 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..0e8771e99c3 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() { @@ -132,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();