From eb833723cd1d3ae600386451312945b9767b6ebc Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Sat, 8 Aug 2015 18:34:19 -0400 Subject: [PATCH 01/14] initial logarithmic scale --- samples/line-logarithmic.html | 154 ++++++++ src/scales/scale.logarithmic.js | 643 ++++++++++++++++++++++++++++++++ 2 files changed, 797 insertions(+) create mode 100644 samples/line-logarithmic.html create mode 100644 src/scales/scale.logarithmic.js diff --git a/samples/line-logarithmic.html b/samples/line-logarithmic.html new file mode 100644 index 000000000..1123c2300 --- /dev/null +++ b/samples/line-logarithmic.html @@ -0,0 +1,154 @@ + + + + + Line Chart + + + + + + +
+ +
+
+
+ + + + + +
+

Legend

+
+ +
+
+ + + + diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js new file mode 100644 index 000000000..68897a950 --- /dev/null +++ b/src/scales/scale.logarithmic.js @@ -0,0 +1,643 @@ +(function() { + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + var defaultConfig = { + display: true, + position: "left", + + // grid line settings + gridLines: { + show: true, + color: "rgba(0, 0, 0, 0.1)", + lineWidth: 1, + drawOnChartArea: true, + drawTicks: true, // draw ticks extending towards the label + zeroLineWidth: 1, + zeroLineColor: "rgba(0,0,0,0.25)", + }, + + // scale numbers + reverse: false, + beginAtZero: false, + override: null, + + // label settings + labels: { + show: true, + mirror: false, + padding: 10, + template: "<%=value.toExponential()%>", + fontSize: 12, + fontStyle: "normal", + fontColor: "#666", + fontFamily: "Helvetica Neue" + } + }; + + var LogarithmicScale = Chart.Element.extend({ + isHorizontal: function() { + return this.options.position == "top" || this.options.position == "bottom"; + }, + generateTicks: function(width, height) { + // We need to decide how many ticks we are going to have. Each tick draws a grid line. + // There are two possibilities. The first is that the user has manually overridden the scale + // calculations in which case the job is easy. The other case is that we have to do it ourselves + // + // We assume at this point that the scale object has been updated with the following values + // by the chart. + // min: this is the minimum value of the scale + // max: this is the maximum value of the scale + // options: contains the options for the scale. This is referenced from the user settings + // rather than being cloned. This ensures that updates always propogate to a redraw + + // Reset the ticks array. Later on, we will draw a grid line at these positions + // The array simply contains the numerical value of the spots where ticks will be + this.ticks = []; + + if (this.options.override) { + // The user has specified the manual override. We use <= instead of < so that + // we get the final line + for (var i = 0; i <= this.options.override.steps; ++i) { + var value = this.options.override.start + (i * this.options.override.stepWidth); + this.ticks.push(value); + } + } else { + // Figure out what the max number of ticks we can support it is based on the size of + // the axis area. For now, we say that the minimum tick spacing in pixels must be 50 + // We also limit the maximum number of ticks to 11 which gives a nice 10 squares on + // the graph + + var minExponent = Math.floor(helpers.log10(this.min)); + var maxExponent = Math.ceil(helpers.log10(this.max)); + + for (var exponent = minExponent; exponent < maxExponent; ++exponent) { + /*this.ticks.push(1.0 * Math.pow(10, exponent)); + this.ticks.push(2.0 * Math.pow(10, exponent)); + this.ticks.push(5.0 * Math.pow(10, exponent));*/ + for (var i = 1; i < 10; ++i) { + if (i === 1 || i === 2 || i === 3 || i === 5 || i === 7) { + this.ticks.push(i * Math.pow(10, exponent)); + } + } + } + + this.ticks.push(1.0 * Math.pow(10, maxExponent)); + + /*var maxTicks; + + if (this.isHorizontal()) { + maxTicks = Math.min(11, Math.ceil(width / 50)); + } else { + // The factor of 2 used to scale the font size has been experimentally determined. + maxTicks = Math.min(11, Math.ceil(height / (2 * this.options.labels.fontSize))); + } + + // Make sure we always have at least 2 ticks + maxTicks = Math.max(2, maxTicks); + + // To get a "nice" value for the tick spacing, we will use the appropriately named + // "nice number" algorithm. See http://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks + // for details. + + // If we are forcing it to begin at 0, but 0 will already be rendered on the chart, + // do nothing since that would make the chart weird. If the user really wants a weird chart + // axis, they can manually override it + if (this.options.beginAtZero) { + var minSign = helpers.sign(this.min); + var maxSign = helpers.sign(this.max); + + if (minSign < 0 && maxSign < 0) { + // move the top up to 0 + this.max = 0; + } else if (minSign > 0 && maxSign > 0) { + // move the botttom down to 0 + this.min = 0; + } + } + + var niceRange = helpers.niceNum(this.max - this.min, false); + var spacing = helpers.niceNum(niceRange / (maxTicks - 1), true); + var niceMin = Math.floor(this.min / spacing) * spacing; + var niceMax = Math.ceil(this.max / spacing) * spacing; + + // Put the values into the ticks array + for (var j = niceMin; j <= niceMax; j += spacing) { + this.ticks.push(j); + }*/ + } + + if (this.options.position == "left" || this.options.position == "right") { + // We are in a vertical orientation. The top value is the highest. So reverse the array + this.ticks.reverse(); + } + + // At this point, we need to update our max and min given the tick values since we have expanded the + // range of the scale + this.max = helpers.max(this.ticks); + this.min = helpers.min(this.ticks); + + if (this.options.reverse) { + this.ticks.reverse(); + + this.start = this.max; + this.end = this.min; + } else { + this.start = this.min; + this.end = this.max; + } + }, + buildLabels: function() { + // We assume that this has been run after ticks have been generated. We try to figure out + // a label for each tick. + this.labels = []; + + helpers.each(this.ticks, function(tick, index, ticks) { + var label; + + if (this.options.labels.userCallback) { + // If the user provided a callback for label generation, use that as first priority + label = this.options.labels.userCallback(tick, index, ticks); + } else if (this.options.labels.template) { + // else fall back to the template string + label = helpers.template(this.options.labels.template, { + value: tick + }); + } + + this.labels.push(label ? label : ""); // empty string will not render so we're good + }, this); + }, + // 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; + }, + getPixelForValue: function(value) { + // This must be called after fit has been run so that + // this.left, this.top, this.right, and this.bottom have been defined + var pixel; + var range = helpers.log10(this.end) - helpers.log10(this.start); + + if (this.isHorizontal()) { + if (value === 0) { + pixel = this.left + this.paddingLeft; + } else { + var innerWidth = this.width - (this.paddingLeft + this.paddingRight); + pixel = this.left + (innerWidth / range * (helpers.log10(value) - helpers.log10(this.start))); + pixel += this.paddingLeft; + } + } else { + // Bottom - top since pixels increase downard on a screen + if (value === 0) { + pixel = this.top + this.paddingTop; + } else { + var innerHeight = this.height - (this.paddingTop + this.paddingBottom); + pixel = this.bottom - (innerHeight / range * (helpers.log10(value) - helpers.log10(this.start))); + pixel += this.paddingTop; + } + } + + return pixel; + }, + + // Functions needed for line charts + calculateRange: function() { + this.min = null; + this.max = null; + + var positiveValues = []; + var negativeValues = []; + + if (this.options.stacked) { + helpers.each(this.data.datasets, function(dataset) { + if (this.isHorizontal() ? dataset.xAxisID === this.id : dataset.yAxisID === this.id) { + helpers.each(dataset.data, function(rawValue, index) { + + var value = this.getRightValue(rawValue); + + positiveValues[index] = positiveValues[index] || 0; + negativeValues[index] = negativeValues[index] || 0; + + if (this.options.relativePoints) { + positiveValues[index] = 100; + } else { + if (value < 0) { + negativeValues[index] += value; + } else { + positiveValues[index] += value; + } + } + }, this); + } + }, this); + + var values = positiveValues.concat(negativeValues); + this.min = helpers.min(values); + this.max = helpers.max(values); + + } else { + helpers.each(this.data.datasets, function(dataset) { + if (this.isHorizontal() ? dataset.xAxisID === this.id : dataset.yAxisID === this.id) { + helpers.each(dataset.data, function(rawValue, index) { + var value = this.getRightValue(rawValue); + + if (this.min === null) { + this.min = value; + } else if (value < this.min) { + this.min = value; + } + + if (this.max === null) { + this.max = value; + } else if (value > this.max) { + this.max = value; + } + }, this); + } + }, this); + } + + if (this.min === this.max) { + this.min--; + this.max++; + } + }, + + getPointPixelForValue: function(rawValue, index, datasetIndex) { + var value = this.getRightValue(rawValue); + + if (this.options.stacked) { + var offsetPos = 0; + var offsetNeg = 0; + + for (var i = this.data.datasets.length - 1; i > datasetIndex; --i) { + if (this.data.datasets[i].data[index] < 0) { + offsetNeg += this.data.datasets[i].data[index]; + } else { + offsetPos += this.data.datasets[i].data[index]; + } + } + + if (value < 0) { + return this.getPixelForValue(offsetNeg + value); + } else { + return this.getPixelForValue(offsetPos + value); + } + } else { + return this.getPixelForValue(value); + } + }, + + // Functions needed for bar charts + calculateBarBase: function(datasetIndex, index) { + var base = 0; + + if (this.options.stacked) { + + var value = this.data.datasets[datasetIndex].data[index]; + + if (value < 0) { + for (var i = 0; i < datasetIndex; i++) { + if (this.data.datasets[i].yAxisID === this.id) { + base += this.data.datasets[i].data[index] < 0 ? this.data.datasets[i].data[index] : 0; + } + } + } else { + for (var j = 0; j < datasetIndex; j++) { + if (this.data.datasets[j].yAxisID === this.id) { + base += this.data.datasets[j].data[index] > 0 ? this.data.datasets[j].data[index] : 0; + } + } + } + + return this.getPixelForValue(base); + } + + base = this.getPixelForValue(this.min); + + if (this.beginAtZero || ((this.min <= 0 && this.max >= 0) || (this.min >= 0 && this.max <= 0))) { + base = this.getPixelForValue(0); + base += this.options.gridLines.lineWidth; + } else if (this.min < 0 && this.max < 0) { + // All values are negative. Use the top as the base + base = this.getPixelForValue(this.max); + } + + return base; + + }, + calculateBarY: function(datasetIndex, index) { + var value = this.data.datasets[datasetIndex].data[index]; + + if (this.options.stacked) { + + var sumPos = 0, + sumNeg = 0; + + for (var i = 0; i < datasetIndex; i++) { + if (this.data.datasets[i].data[index] < 0) { + sumNeg += this.data.datasets[i].data[index] || 0; + } else { + sumPos += this.data.datasets[i].data[index] || 0; + } + } + + if (value < 0) { + return this.getPixelForValue(sumNeg + value); + } else { + return this.getPixelForValue(sumPos + value); + } + + return this.getPixelForValue(value); + } + + var offset = 0; + + for (var j = datasetIndex; j < this.data.datasets.length; j++) { + if (j === datasetIndex && value) { + offset += value; + } else { + offset = offset + value; + } + } + + return this.getPixelForValue(value); + }, + + // Fit this axis to the given size + // @param {number} maxWidth : the max width the axis can be + // @param {number} maxHeight: the max height the axis can be + // @return {object} minSize : the minimum size needed to draw the axis + fit: function(maxWidth, maxHeight, margins) { + this.calculateRange(); + this.generateTicks(maxWidth, maxHeight); + this.buildLabels(); + + var minSize = { + width: 0, + height: 0, + }; + + // In a horizontal axis, we need some room for the scale to be drawn + // + // ----------------------------------------------------- + // | | | | | + // + // In a vertical axis, we need some room for the scale to be drawn. + // The actual grid lines will be drawn on the chart area, however, we need to show + // ticks where the axis actually is. + // We will allocate 25px for this width + // | + // -| + // | + // | + // -| + // | + // | + // -| + + + // Width + if (this.isHorizontal()) { + minSize.width = maxWidth; // fill all the width + } else { + minSize.width = this.options.gridLines.show && this.options.display ? 10 : 0; + } + + // height + if (this.isHorizontal()) { + minSize.height = this.options.gridLines.show && this.options.display ? 10 : 0; + } else { + minSize.height = maxHeight; // fill all the height + } + + this.paddingLeft = 0; + this.paddingRight = 0; + this.paddingTop = 0; + this.paddingBottom = 0; + + + if (this.options.labels.show && this.options.display) { + // Don't bother fitting the labels if we are not showing them + var labelFont = helpers.fontString(this.options.labels.fontSize, + this.options.labels.fontStyle, this.options.labels.fontFamily); + + if (this.isHorizontal()) { + // A horizontal axis is more constrained by the height. + var maxLabelHeight = maxHeight - minSize.height; + var labelHeight = 1.5 * this.options.labels.fontSize; + minSize.height = Math.min(maxHeight, minSize.height + labelHeight); + + var labelFont = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily); + this.ctx.font = labelFont; + + var firstLabelWidth = this.ctx.measureText(this.labels[0]).width; + var lastLabelWidth = this.ctx.measureText(this.labels[this.labels.length - 1]).width; + + // Ensure that our labels are always inside the canvas + this.paddingLeft = firstLabelWidth / 2; + this.paddingRight = lastLabelWidth / 2; + } else { + // A vertical axis is more constrained by the width. Labels are the dominant factor + // here, so get that length first + var maxLabelWidth = maxWidth - minSize.width; + var largestTextWidth = helpers.longestText(this.ctx, labelFont, this.labels); + + if (largestTextWidth < maxLabelWidth) { + // We don't need all the room + minSize.width += largestTextWidth; + minSize.width += 3; // extra padding + } else { + // Expand to max size + minSize.width = maxWidth; + } + + this.paddingTop = this.options.labels.fontSize / 2; + this.paddingBottom = this.options.labels.fontSize / 2; + } + } + + if (margins) { + this.paddingLeft -= margins.left; + this.paddingTop -= margins.top; + this.paddingRight -= margins.right; + this.paddingBottom -= margins.bottom; + + this.paddingLeft = Math.max(this.paddingLeft, 0); + this.paddingTop = Math.max(this.paddingTop, 0); + this.paddingRight = Math.max(this.paddingRight, 0); + this.paddingBottom = Math.max(this.paddingBottom, 0); + } + + this.width = minSize.width; + this.height = minSize.height; + return minSize; + }, + // Actualy draw the scale on the canvas + // @param {rectangle} chartArea : the area of the chart to draw full grid lines on + draw: function(chartArea) { + if (this.options.display) { + + var setContextLineSettings; + var hasZero; + + // Make sure we draw text in the correct color + this.ctx.fillStyle = this.options.labels.fontColor; + + if (this.isHorizontal()) { + if (this.options.gridLines.show) { + // Draw the horizontal line + setContextLineSettings = true; + hasZero = helpers.findNextWhere(this.ticks, function(tick) { + return tick === 0; + }) !== undefined; + var yTickStart = this.options.position == "bottom" ? this.top : this.bottom - 5; + var yTickEnd = this.options.position == "bottom" ? this.top + 5 : this.bottom; + + helpers.each(this.ticks, function(tick, index) { + // Grid lines are vertical + var xValue = this.getPixelForValue(tick); + + if (tick === 0 || (!hasZero && index === 0)) { + // Draw the 0 point specially or the left if there is no 0 + this.ctx.lineWidth = this.options.gridLines.zeroLineWidth; + this.ctx.strokeStyle = this.options.gridLines.zeroLineColor; + setContextLineSettings = true; // reset next time + } else if (setContextLineSettings) { + this.ctx.lineWidth = this.options.gridLines.lineWidth; + this.ctx.strokeStyle = this.options.gridLines.color; + setContextLineSettings = false; + } + + xValue += helpers.aliasPixel(this.ctx.lineWidth); + + // Draw the label area + this.ctx.beginPath(); + + if (this.options.gridLines.drawTicks) { + this.ctx.moveTo(xValue, yTickStart); + this.ctx.lineTo(xValue, yTickEnd); + } + + // Draw the chart area + if (this.options.gridLines.drawOnChartArea) { + this.ctx.moveTo(xValue, chartArea.top); + this.ctx.lineTo(xValue, chartArea.bottom); + } + + // Need to stroke in the loop because we are potentially changing line widths & colours + this.ctx.stroke(); + }, this); + } + + if (this.options.labels.show) { + // Draw the labels + + var labelStartY; + + if (this.options.position == "top") { + labelStartY = this.bottom - 10; + this.ctx.textBaseline = "bottom"; + } else { + // bottom side + labelStartY = this.top + 10; + this.ctx.textBaseline = "top"; + } + + this.ctx.textAlign = "center"; + this.ctx.font = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily); + + helpers.each(this.labels, function(label, index) { + var xValue = this.getPixelForValue(this.ticks[index]); + this.ctx.fillText(label, xValue, labelStartY); + }, this); + } + } else { + // Vertical + if (this.options.gridLines.show) { + + // Draw the vertical line + setContextLineSettings = true; + hasZero = helpers.findNextWhere(this.ticks, function(tick) { + return tick === 0; + }) !== undefined; + var xTickStart = this.options.position == "right" ? this.left : this.right - 5; + var xTickEnd = this.options.position == "right" ? this.left + 5 : this.right; + + helpers.each(this.ticks, function(tick, index) { + // Grid lines are horizontal + var yValue = this.getPixelForValue(tick); + + if (tick === 0 || (!hasZero && index === 0)) { + // Draw the 0 point specially or the bottom if there is no 0 + this.ctx.lineWidth = this.options.gridLines.zeroLineWidth; + this.ctx.strokeStyle = this.options.gridLines.zeroLineColor; + setContextLineSettings = true; // reset next time + } else if (setContextLineSettings) { + this.ctx.lineWidth = this.options.gridLines.lineWidth; + this.ctx.strokeStyle = this.options.gridLines.color; + setContextLineSettings = false; // use boolean to indicate that we only want to do this once + } + + yValue += helpers.aliasPixel(this.ctx.lineWidth); + + // Draw the label area + this.ctx.beginPath(); + + if (this.options.gridLines.drawTicks) { + this.ctx.moveTo(xTickStart, yValue); + this.ctx.lineTo(xTickEnd, yValue); + } + + // Draw the chart area + if (this.options.gridLines.drawOnChartArea) { + this.ctx.moveTo(chartArea.left, yValue); + this.ctx.lineTo(chartArea.right, yValue); + } + + this.ctx.stroke(); + }, this); + } + + if (this.options.labels.show) { + // Draw the labels + + var labelStartX; + + if (this.options.position == "left") { + if (this.options.labels.mirror) { + labelStartX = this.right + this.options.labels.padding; + this.ctx.textAlign = "left"; + } else { + labelStartX = this.right - this.options.labels.padding; + this.ctx.textAlign = "right"; + } + } else { + // right side + if (this.options.labels.mirror) { + labelStartX = this.left - this.options.labels.padding; + this.ctx.textAlign = "right"; + } else { + labelStartX = this.left + this.options.labels.padding; + this.ctx.textAlign = "left"; + } + } + + this.ctx.textBaseline = "middle"; + this.ctx.font = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily); + + helpers.each(this.labels, function(label, index) { + var yValue = this.getPixelForValue(this.ticks[index]); + this.ctx.fillText(label, labelStartX, yValue); + }, this); + } + } + } + } + }); + Chart.scaleService.registerScaleType("logarithmic", LogarithmicScale, defaultConfig); + +}).call(this); From 98eea76195402b0908c7462eb6d40d779fb81a44 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Thu, 10 Sep 2015 22:46:07 -0600 Subject: [PATCH 02/14] Do not test on gulp watch --- gulpfile.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index 42fd09b09..c430de205 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -176,7 +176,10 @@ function moduleSizesTask() { } function watchTask() { - gulp.watch('./src/**', ['build', 'unittest', 'unittestWatch']); + if (util.env.test) { + return gulp.watch('./src/**', ['build', 'unittest', 'unittestWatch']); + } + return gulp.watch('./src/**', ['build']); } function serverTask() { From 8762ae2f2806d7784b1b716e273827544abc53eb Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Thu, 10 Sep 2015 22:46:51 -0600 Subject: [PATCH 03/14] Add moment.js as dependency --- gulpfile.js | 3 ++- package.json | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index c430de205..09ce81eff 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -33,7 +33,8 @@ var srcFiles = [ './src/scales/**', './src/elements/**', './src/charts/**', - './node_modules/color/dist/color.min.js' + './node_modules/color/dist/color.min.js', + './node_modules/moment/min/moment.min.js' ]; diff --git a/package.json b/package.json index 0e7b31939..8b83a5731 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "karma-firefox-launcher": "^0.1.6", "karma-jasmine": "^0.3.6", "karma-jasmine-html-reporter": "^0.1.8", + "moment": "^2.10.6", "onecolor": "^2.5.0", "semver": "^3.0.1" }, From fe5ef1584b7e10b4d584e553ecb949d7c8ccc5ec Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Thu, 10 Sep 2015 22:48:20 -0600 Subject: [PATCH 04/14] Add Line chart with time scale sample --- samples/line-time-scale.html | 162 +++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 samples/line-time-scale.html diff --git a/samples/line-time-scale.html b/samples/line-time-scale.html new file mode 100644 index 000000000..266981ffe --- /dev/null +++ b/samples/line-time-scale.html @@ -0,0 +1,162 @@ + + + + + Line Chart + + + + + + +
+ +
+
+
+ + + + + +
+

Legend

+
+
+
+ + + + From c2a7e4c251ccc117a7cdfe0a0947eb86652350f8 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Thu, 10 Sep 2015 22:48:48 -0600 Subject: [PATCH 05/14] Time Scale Base --- src/scales/scale.time.js | 405 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 405 insertions(+) create mode 100644 src/scales/scale.time.js diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js new file mode 100644 index 000000000..dc36742ac --- /dev/null +++ b/src/scales/scale.time.js @@ -0,0 +1,405 @@ +(function() { + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + var defaultConfig = { + display: true, + position: "bottom", + + // grid line settings + gridLines: { + show: true, + color: "rgba(0, 0, 0, 0.1)", + lineWidth: 1, + drawOnChartArea: true, + drawTicks: true, // draw ticks extending towards the label + }, + + time: { + format: false, // http://momentjs.com/docs/#/parsing/string-format/ + unit: false, // week, month, year, etc. + aggregation: 'average', + display: false, //http://momentjs.com/docs/#/parsing/string-format/ + unitFormats: { + 'millisecond': 'h:mm:ss SSS', // 11:20:01 002 + 'second': 'h:mm:ss', // 11:20:01 + 'minute': 'h:mm:ss a', // 11:20:01 AM + 'hour': 'MMM D, hA', // Sept 4, 5PM + 'day': 'll', // Sep 4 2015 8:30 PM + 'week': '[W]WW - YYYY', // Week 46 + 'month': 'MMM YYYY', // Sept 2015 + 'quarter': '[Q]Q - YYYY', // Q3 + 'year': 'YYYY' // 2015 + } + }, + + // scale numbers + reverse: false, + override: null, + + // label settings + labels: { + show: true, + mirror: false, + padding: 10, + template: "<%=value.toLocaleString()%>", + fontSize: 12, + fontStyle: "normal", + fontColor: "#666", + fontFamily: "Helvetica Neue" + } + }; + + var TimeScale = Chart.Element.extend({ + isHorizontal: function() { + return this.options.position == "top" || this.options.position == "bottom"; + }, + parseTime: function(label) { + if (typeof this.options.time.format !== 'string' && this.options.time.format.call) { + return this.options.time.format(label); + } else { + return moment(label, this.options.time.format); + } + }, + buildLabels: function(index) { + // Actual labels on the grid + this.labels = []; + // A map of original labelIndex to time labelIndex + this.timeLabelIndexMap = {}; + + var definedMoments = []; + + // Format each point into a moment + this.data.labels.forEach(function(label, index) { + definedMoments.push(this.parseTime(label)); + }, this); + + + // Find or set the unit of time + if (!this.options.time.unit) { + + // Determine the smallest needed unit of the time + helpers.each([ + 'millisecond', + 'second', + 'minute', + 'hour', + 'day', + 'week', + 'month', + 'quarter', + 'year', + ], function(format) { + if (this.timeUnit) { + return; + } + var start; + helpers.each(definedMoments, function(mom) { + if (!start) { + start = mom[format](); + } + + if (mom[format]() !== start) { + this.timeUnit = format; + if (!this.displayFormat) { + this.displayFormat = this.options.time.unitFormats[format]; + } + } + }, this); + }, this); + } else { + this.timeUnit = this.options.time.unit; + } + + if (!this.timeUnit) { + this.timeUnit = 'day'; + this.displayFormat = this.options.time.unitFormats.day; + } + + + if (this.options.time.display) { + this.displayFormat = this.options.time.display; + } + + + // Find the first and last moments + this.firstMoment = moment.min.call(this, definedMoments); + this.lastMoment = moment.max.call(this, definedMoments); + + // Find the length of the timeframe in the desired unit + var momentRangeLength = this.lastMoment.diff(this.firstMoment, this.timeUnit); + + helpers.each(definedMoments, function(definedMoment, index) { + this.timeLabelIndexMap[index] = momentRangeLength - this.lastMoment.diff(definedMoment, this.timeUnit); + }, this); + + // For every unit in between the first and last moment, create a moment and add it to the labels tick + var i = 0; + if (this.options.labels.userCallback) { + for (; i <= momentRangeLength; i++) { + this.labels.push( + this.options.labels.userCallback(this.firstMoment + .add((!i ? 0 : 1), this.timeUnit) + .format(this.options.time.display ? this.options.time.display : this.displayFormat) + ) + ); + } + } else { + for (; i <= momentRangeLength; i++) { + this.labels.push(this.firstMoment + .add((!i ? 0 : 1), this.timeUnit) + .format(this.options.time.display ? this.options.time.display : this.displayFormat) + ); + } + } + + + }, + getPixelForValue: function(value, index, datasetIndex, includeOffset) { + // This must be called after fit has been run so that + // this.left, this.top, this.right, and this.bottom have been defined + if (this.isHorizontal()) { + var innerWidth = this.width - (this.paddingLeft + this.paddingRight); + var valueWidth = innerWidth / Math.max((this.labels.length - ((this.options.gridLines.offsetGridLines) ? 0 : 1)), 1); + var valueOffset = (valueWidth * index) + this.paddingLeft; + + if (this.options.gridLines.offsetGridLines && includeOffset) { + valueOffset += (valueWidth / 2); + } + + return this.left + Math.round(valueOffset); + } else { + return this.top + (index * (this.height / this.labels.length)); + } + }, + getPointPixelForValue: function(value, index, datasetIndex) { + // This function references the timeLaabelIndexMap to know which index in the timeLabels corresponds to the index of original labels + return this.getPixelForValue(value, this.timeLabelIndexMap[index], datasetIndex, true); + }, + + // Functions needed for bar charts + calculateBaseWidth: function() { + return (this.getPixelForValue(null, 1, 0, true) - this.getPixelForValue(null, 0, 0, true)) - (2 * this.options.categorySpacing); + }, + calculateBarWidth: function(barDatasetCount) { + //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset + var baseWidth = this.calculateBaseWidth() - ((barDatasetCount - 1) * this.options.spacing); + + if (this.options.stacked) { + return baseWidth; + } + return (baseWidth / barDatasetCount); + }, + calculateBarX: function(barDatasetCount, datasetIndex, elementIndex) { + var xWidth = this.calculateBaseWidth(), + xAbsolute = this.getPixelForValue(null, elementIndex, datasetIndex, true) - (xWidth / 2), + barWidth = this.calculateBarWidth(barDatasetCount); + + if (this.options.stacked) { + return xAbsolute + barWidth / 2; + } + + return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * this.options.spacing) + barWidth / 2; + }, + + calculateLabelRotation: function(maxHeight, margins) { + //Get the width of each grid by calculating the difference + //between x offsets between 0 and 1. + var labelFont = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily); + this.ctx.font = labelFont; + + var firstWidth = this.ctx.measureText(this.labels[0]).width; + var lastWidth = this.ctx.measureText(this.labels[this.labels.length - 1]).width; + var firstRotated; + var lastRotated; + + this.paddingRight = lastWidth / 2 + 3; + this.paddingLeft = firstWidth / 2 + 3; + + this.labelRotation = 0; + + if (this.options.display) { + var originalLabelWidth = helpers.longestText(this.ctx, labelFont, this.labels); + var cosRotation; + var sinRotation; + var firstRotatedWidth; + + this.labelWidth = originalLabelWidth; + + //Allow 3 pixels x2 padding either side for label readability + // only the index matters for a dataset scale, but we want a consistent interface between scales + + var datasetWidth = Math.floor(this.getPixelForValue(0, 1) - this.getPixelForValue(0, 0)) - 6; + + //Max label rotation can be set or default to 90 - also act as a loop counter + while (this.labelWidth > datasetWidth && this.labelRotation <= this.options.labels.maxRotation) { + cosRotation = Math.cos(helpers.toRadians(this.labelRotation)); + sinRotation = Math.sin(helpers.toRadians(this.labelRotation)); + + firstRotated = cosRotation * firstWidth; + lastRotated = cosRotation * lastWidth; + + // We're right aligning the text now. + if (firstRotated + this.options.labels.fontSize / 2 > this.yLabelWidth) { + this.paddingLeft = firstRotated + this.options.labels.fontSize / 2; + } + + this.paddingRight = this.options.labels.fontSize / 2; + + if (sinRotation * originalLabelWidth > maxHeight) { + // go back one step + this.labelRotation--; + break; + } + + this.labelRotation++; + this.labelWidth = cosRotation * originalLabelWidth; + + } + } else { + this.labelWidth = 0; + this.paddingRight = 0; + this.paddingLeft = 0; + } + + if (margins) { + this.paddingLeft -= margins.left; + this.paddingRight -= margins.right; + + this.paddingLeft = Math.max(this.paddingLeft, 0); + this.paddingRight = Math.max(this.paddingRight, 0); + } + + }, + // Fit this axis to the given size + // @param {number} maxWidth : the max width the axis can be + // @param {number} maxHeight: the max height the axis can be + // @return {object} minSize : the minimum size needed to draw the axis + fit: function(maxWidth, maxHeight, margins) { + // Set the unconstrained dimension before label rotation + if (this.isHorizontal()) { + this.width = maxWidth; + } else { + this.height = maxHeight; + } + + this.buildLabels(); + this.calculateLabelRotation(maxHeight, margins); + + var minSize = { + width: 0, + height: 0, + }; + + var labelFont = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily); + var longestLabelWidth = helpers.longestText(this.ctx, labelFont, this.labels); + + // Width + if (this.isHorizontal()) { + minSize.width = maxWidth; + } else if (this.options.display) { + var labelWidth = this.options.labels.show ? longestLabelWidth + 6 : 0; + minSize.width = Math.min(labelWidth, maxWidth); + } + + // Height + if (this.isHorizontal() && this.options.display) { + var labelHeight = (Math.sin(helpers.toRadians(this.labelRotation)) * longestLabelWidth) + 1.5 * this.options.labels.fontSize; + minSize.height = Math.min(this.options.labels.show ? labelHeight : 0, maxHeight); + } else if (this.options.display) { + minSize.height = maxHeight; + } + + this.width = minSize.width; + this.height = minSize.height; + return minSize; + }, + // Actualy draw the scale on the canvas + // @param {rectangle} chartArea : the area of the chart to draw full grid lines on + draw: function(chartArea) { + if (this.options.display) { + + var setContextLineSettings; + + // Make sure we draw text in the correct color + this.ctx.fillStyle = this.options.labels.fontColor; + + if (this.isHorizontal()) { + setContextLineSettings = true; + var yTickStart = this.options.position == "bottom" ? this.top : this.bottom - 10; + var yTickEnd = this.options.position == "bottom" ? this.top + 10 : this.bottom; + var isRotated = this.labelRotation !== 0; + var skipRatio = false; + + if ((this.options.labels.fontSize + 4) * this.labels.length > (this.width - (this.paddingLeft + this.paddingRight))) { + skipRatio = 1 + Math.floor(((this.options.labels.fontSize + 4) * this.labels.length) / (this.width - (this.paddingLeft + this.paddingRight))); + } + + helpers.each(this.labels, function(label, index) { + // Blank labels + if ((skipRatio > 1 && index % skipRatio > 0) || (label === undefined || label === null)) { + return; + } + var xLineValue = this.getPixelForValue(label, index, null, false); // xvalues for grid lines + var xLabelValue = this.getPixelForValue(label, index, null, true); // x values for labels (need to consider offsetLabel option) + + if (this.options.gridLines.show) { + if (index === 0) { + // Draw the first index specially + this.ctx.lineWidth = this.options.gridLines.zeroLineWidth; + this.ctx.strokeStyle = this.options.gridLines.zeroLineColor; + setContextLineSettings = true; // reset next time + } else if (setContextLineSettings) { + this.ctx.lineWidth = this.options.gridLines.lineWidth; + this.ctx.strokeStyle = this.options.gridLines.color; + setContextLineSettings = false; + } + + xLineValue += helpers.aliasPixel(this.ctx.lineWidth); + + // Draw the label area + this.ctx.beginPath(); + + if (this.options.gridLines.drawTicks) { + this.ctx.moveTo(xLineValue, yTickStart); + this.ctx.lineTo(xLineValue, yTickEnd); + } + + // Draw the chart area + if (this.options.gridLines.drawOnChartArea) { + this.ctx.moveTo(xLineValue, chartArea.top); + this.ctx.lineTo(xLineValue, chartArea.bottom); + } + + // Need to stroke in the loop because we are potentially changing line widths & colours + this.ctx.stroke(); + } + + if (this.options.labels.show) { + this.ctx.save(); + this.ctx.translate(xLabelValue, (isRotated) ? this.top + 12 : this.top + 8); + this.ctx.rotate(helpers.toRadians(this.labelRotation) * -1); + this.ctx.font = this.font; + this.ctx.textAlign = (isRotated) ? "right" : "center"; + this.ctx.textBaseline = (isRotated) ? "middle" : "top"; + this.ctx.fillText(label, 0, 0); + this.ctx.restore(); + } + }, this); + } else { + // Vertical + if (this.options.gridLines.show) {} + + if (this.options.labels.show) { + // Draw the labels + } + } + } + } + }); + Chart.scaleService.registerScaleType("time", TimeScale, defaultConfig); + +}).call(this); From 88d30d8c935bdeddc589fe7bc736be1263863b68 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Fri, 11 Sep 2015 12:20:32 -0600 Subject: [PATCH 06/14] Let tooltips use time formatted labels if they exist --- src/core/core.tooltip.js | 6 +++++- src/scales/scale.time.js | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/core/core.tooltip.js b/src/core/core.tooltip.js index 3f4a892c4..92e2b1364 100644 --- a/src/core/core.tooltip.js +++ b/src/core/core.tooltip.js @@ -164,7 +164,11 @@ x: medianPosition.x, y: medianPosition.y, labels: labels, - title: this._data.labels && this._data.labels.length ? this._data.labels[this._active[0]._index] : '', + 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, }); diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index dc36742ac..cbf6473ca 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -69,6 +69,8 @@ this.labels = []; // A map of original labelIndex to time labelIndex this.timeLabelIndexMap = {}; + // The time formatted versions of the labels for use by tooltips + this.data.timeLabels = []; var definedMoments = []; @@ -134,6 +136,10 @@ helpers.each(definedMoments, function(definedMoment, index) { this.timeLabelIndexMap[index] = momentRangeLength - this.lastMoment.diff(definedMoment, this.timeUnit); + this.data.timeLabels.push( + definedMoment + .format(this.options.time.display ? this.options.time.display : this.displayFormat) + ); }, this); // For every unit in between the first and last moment, create a moment and add it to the labels tick From b172a382f1f04b7c163d336364cdfcf1960a8a9d Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Mon, 14 Sep 2015 19:45:13 -0400 Subject: [PATCH 07/14] Remove dead code in logarithmic scale. Fixed a padding bug that had previously been fixed in the linear scale --- src/scales/scale.logarithmic.js | 48 +-------------------------------- 1 file changed, 1 insertion(+), 47 deletions(-) diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 68897a950..7294cd522 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -75,9 +75,6 @@ var maxExponent = Math.ceil(helpers.log10(this.max)); for (var exponent = minExponent; exponent < maxExponent; ++exponent) { - /*this.ticks.push(1.0 * Math.pow(10, exponent)); - this.ticks.push(2.0 * Math.pow(10, exponent)); - this.ticks.push(5.0 * Math.pow(10, exponent));*/ for (var i = 1; i < 10; ++i) { if (i === 1 || i === 2 || i === 3 || i === 5 || i === 7) { this.ticks.push(i * Math.pow(10, exponent)); @@ -86,48 +83,6 @@ } this.ticks.push(1.0 * Math.pow(10, maxExponent)); - - /*var maxTicks; - - if (this.isHorizontal()) { - maxTicks = Math.min(11, Math.ceil(width / 50)); - } else { - // The factor of 2 used to scale the font size has been experimentally determined. - maxTicks = Math.min(11, Math.ceil(height / (2 * this.options.labels.fontSize))); - } - - // Make sure we always have at least 2 ticks - maxTicks = Math.max(2, maxTicks); - - // To get a "nice" value for the tick spacing, we will use the appropriately named - // "nice number" algorithm. See http://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks - // for details. - - // If we are forcing it to begin at 0, but 0 will already be rendered on the chart, - // do nothing since that would make the chart weird. If the user really wants a weird chart - // axis, they can manually override it - if (this.options.beginAtZero) { - var minSign = helpers.sign(this.min); - var maxSign = helpers.sign(this.max); - - if (minSign < 0 && maxSign < 0) { - // move the top up to 0 - this.max = 0; - } else if (minSign > 0 && maxSign > 0) { - // move the botttom down to 0 - this.min = 0; - } - } - - var niceRange = helpers.niceNum(this.max - this.min, false); - var spacing = helpers.niceNum(niceRange / (maxTicks - 1), true); - var niceMin = Math.floor(this.min / spacing) * spacing; - var niceMax = Math.ceil(this.max / spacing) * spacing; - - // Put the values into the ticks array - for (var j = niceMin; j <= niceMax; j += spacing) { - this.ticks.push(j); - }*/ } if (this.options.position == "left" || this.options.position == "right") { @@ -195,8 +150,7 @@ pixel = this.top + this.paddingTop; } else { var innerHeight = this.height - (this.paddingTop + this.paddingBottom); - pixel = this.bottom - (innerHeight / range * (helpers.log10(value) - helpers.log10(this.start))); - pixel += this.paddingTop; + pixel = (this.bottom - this.paddingBottom) - (innerHeight / range * (helpers.log10(value) - helpers.log10(this.start))); } } From e0cdfc6d2d3f512258da94ab5fd8b1360a43a138 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Mon, 14 Sep 2015 19:46:28 -0400 Subject: [PATCH 08/14] Update logarithmic sample. Created a new logarithmic sample using AC circuit simulation data of an RLC filter. --- samples/line-logarithmic.html | 2 +- samples/scatter-logX.html | 162 ++++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 samples/scatter-logX.html diff --git a/samples/line-logarithmic.html b/samples/line-logarithmic.html index 1123c2300..8a249ba96 100644 --- a/samples/line-logarithmic.html +++ b/samples/line-logarithmic.html @@ -31,7 +31,7 @@ + + + + +
+
+ +
+
+ + + + From c1f0a39982e0595e2022df9365a68f39173fea20 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Mon, 14 Sep 2015 20:32:23 -0400 Subject: [PATCH 09/14] Fixes to show all lines but not all labels. Added a variable in the template function to make debugging nicer. --- samples/scatter-logX.html | 6 +++++- src/core/core.helpers.js | 7 +++---- src/scales/scale.logarithmic.js | 12 ++++++------ 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/samples/scatter-logX.html b/samples/scatter-logX.html index 976684f5c..d8d00c85c 100644 --- a/samples/scatter-logX.html +++ b/samples/scatter-logX.html @@ -140,7 +140,11 @@ position: 'bottom', labels: { userCallback: function(tick) { - return tick.toString() + "Hz"; + var remain = tick / (Math.pow(10, Math.floor(Chart.helpers.log10(tick)))); + if (remain === 1 || remain === 2 || remain === 5) { + return tick.toString() + "Hz"; + } + return ''; } } }], diff --git a/src/core/core.helpers.js b/src/core/core.helpers.js index 46ad752d8..ab58b0789 100644 --- a/src/core/core.helpers.js +++ b/src/core/core.helpers.js @@ -387,8 +387,7 @@ } else { // Generate a reusable function that will serve as a template // generator (and which will be cached). - fn = new Function("obj", - "var p=[],print=function(){p.push.apply(p,arguments);};" + + var functionCode = "var p=[],print=function(){p.push.apply(p,arguments);};" + // Introduce the data as local variables using with(){} "with(obj){p.push('" + @@ -402,8 +401,8 @@ .split("\t").join("');") .split("%>").join("p.push('") .split("\r").join("\\'") + - "');}return p.join('');" - ); + "');}return p.join('');"; + fn = new Function("obj", functionCode); // Cache the result templateStringCache[str] = fn; diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 7294cd522..a8d9706b9 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -30,7 +30,7 @@ show: true, mirror: false, padding: 10, - template: "<%=value.toExponential()%>", + template: "<%var remain = value / (Math.pow(10, Math.floor(Chart.helpers.log10(value))));if (remain === 1 || remain === 2 || remain === 5) {%><%=value.toExponential()%><%} else {%><%= null %><%}%>", fontSize: 12, fontStyle: "normal", fontColor: "#666", @@ -76,9 +76,7 @@ for (var exponent = minExponent; exponent < maxExponent; ++exponent) { for (var i = 1; i < 10; ++i) { - if (i === 1 || i === 2 || i === 3 || i === 5 || i === 7) { - this.ticks.push(i * Math.pow(10, exponent)); - } + this.ticks.push(i * Math.pow(10, exponent)); } } @@ -123,7 +121,7 @@ }); } - this.labels.push(label ? label : ""); // empty string will not render so we're good + this.labels.push(label); // empty string will not render so we're good }, this); }, // Get the correct value. If the value type is object get the x or y based on whether we are horizontal or not @@ -506,7 +504,9 @@ helpers.each(this.labels, function(label, index) { var xValue = this.getPixelForValue(this.ticks[index]); - this.ctx.fillText(label, xValue, labelStartY); + if (label) { + this.ctx.fillText(label, xValue, labelStartY); + } }, this); } } else { From eb657bce36f470d632c95072f0a0bcdafb4933f8 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Mon, 14 Sep 2015 20:34:49 -0400 Subject: [PATCH 10/14] Hide labels when the user returns null from the userCallback. This is the same as the category scale --- src/scales/scale.logarithmic.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index a8d9706b9..7d0f181c9 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -453,6 +453,11 @@ // Grid lines are vertical var xValue = this.getPixelForValue(tick); + if (this.labels[index] === null) { + // If the user specifically hid the label by returning null from the label function, do so + return; + } + if (tick === 0 || (!hasZero && index === 0)) { // Draw the 0 point specially or the left if there is no 0 this.ctx.lineWidth = this.options.gridLines.zeroLineWidth; From 2598446d54de0e15f832729843e1d2c9bc244e35 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Tue, 15 Sep 2015 11:40:01 -0600 Subject: [PATCH 11/14] Time Scale Rewrite --- samples/line-time-scale.html | 23 ++- src/scales/scale.time.js | 264 +++++++++++++++++++---------------- 2 files changed, 164 insertions(+), 123 deletions(-) diff --git a/samples/line-time-scale.html b/samples/line-time-scale.html index 266981ffe..01df3a4e2 100644 --- a/samples/line-time-scale.html +++ b/samples/line-time-scale.html @@ -38,12 +38,21 @@ var randomColor = function(opacity) { return 'rgba(' + randomColorFactor() + ',' + randomColorFactor() + ',' + randomColorFactor() + ',' + (opacity || '.3') + ')'; }; + var newDate = function(days) { + var date = new Date(); + return date.setDate(date.getDate() + days); + }; + var newTimestamp = function(days) { + return Date.now() - days * 100000; + }; var config = { type: 'line', data: { - // labels: ["01/01/2015 20:00", "01/02/2015 21:00", "01/03/2015 22:00", "01/06/2015 23:00", "01/15/2015 03:00", "01/17/2015 10:00", "01/30/2015 11:00"], // Hours - labels: ["01/01/2015", "01/02/2015", "01/03/2015", "01/06/2015", "01/15/2015", "01/17/2015", "01/30/2015"], // Days + //labels: [newTimestamp(0), newTimestamp(1), newTimestamp(2), newTimestamp(3), newTimestamp(4), newTimestamp(5), newTimestamp(6)], // unix timestamps + // labels: [newDate(0), newDate(1), newDate(2), newDate(3), newDate(4), newDate(5), newDate(6)], // Date Objects + labels: ["01/01/2015 20:00", "01/02/2015 21:00", "01/03/2015 22:00", "01/05/2015 23:00", "01/07/2015 03:00", "01/08/2015 10:00", "02/1/2015"], // Hours + // labels: ["01/01/2015", "01/02/2015", "01/03/2015", "01/06/2015", "01/15/2015", "01/17/2015", "01/30/2015"], // Days // labels: ["12/25/2014", "01/08/2015", "01/15/2015", "01/22/2015", "01/29/2015", "02/05/2015", "02/12/2015"], // Weeks datasets: [{ label: "My First dataset", @@ -61,14 +70,20 @@ xAxes: [{ type: "time", display: true, - time: { + tick: { format: 'MM/DD/YYYY HH:mm', + // round: 'day' } }, ], yAxes: [{ display: true }] - } + }, + elements: { + line: { + tension: 0 + } + }, } }; diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js index cbf6473ca..18df1a6ac 100644 --- a/src/scales/scale.time.js +++ b/src/scales/scale.time.js @@ -5,6 +5,58 @@ Chart = root.Chart, helpers = Chart.helpers; + var time = { + units: [ + 'millisecond', + 'second', + 'minute', + 'hour', + 'day', + 'week', + 'month', + 'quarter', + 'year', + ], + unit: { + 'millisecond': { + display: 'SSS [ms]', // 002 ms + maxStep: 1000, + }, + 'second': { + display: 'h:mm:ss a', // 11:20:01 AM + maxStep: 60, + }, + 'minute': { + display: 'h:mm:ss a', // 11:20:01 AM + maxStep: 60, + }, + 'hour': { + display: 'MMM D, hA', // Sept 4, 5PM + maxStep: 24, + }, + 'day': { + display: 'll', // Sep 4 2015 + maxStep: 7, + }, + 'week': { + display: 'll', // Week 46, or maybe "[W]WW - YYYY" ? + maxStep: 4.3333, + }, + 'month': { + display: 'MMM YYYY', // Sept 2015 + maxStep: 12, + }, + 'quarter': { + display: '[Q]Q - YYYY', // Q3 + maxStep: 4, + }, + 'year': { + display: 'YYYY', // 2015 + maxStep: false, + }, + } + }; + var defaultConfig = { display: true, position: "bottom", @@ -18,22 +70,11 @@ drawTicks: true, // draw ticks extending towards the label }, - time: { - format: false, // http://momentjs.com/docs/#/parsing/string-format/ - unit: false, // week, month, year, etc. - aggregation: 'average', - display: false, //http://momentjs.com/docs/#/parsing/string-format/ - unitFormats: { - 'millisecond': 'h:mm:ss SSS', // 11:20:01 002 - 'second': 'h:mm:ss', // 11:20:01 - 'minute': 'h:mm:ss a', // 11:20:01 AM - 'hour': 'MMM D, hA', // Sept 4, 5PM - 'day': 'll', // Sep 4 2015 8:30 PM - 'week': '[W]WW - YYYY', // Week 46 - 'month': 'MMM YYYY', // Sept 2015 - 'quarter': '[Q]Q - YYYY', // Q3 - 'year': 'YYYY' // 2015 - } + tick: { + format: false, // false == date objects or use pattern string from http://momentjs.com/docs/#/parsing/string-format/ + unit: false, // false == automatic or override with week, month, year, etc. + round: false, // none, or override with week, month, year, etc. + displayFormat: false, // defaults to unit's corresponding unitFormat below or override using pattern string from http://momentjs.com/docs/#/displaying/format/ }, // scale numbers @@ -49,7 +90,8 @@ fontSize: 12, fontStyle: "normal", fontColor: "#666", - fontFamily: "Helvetica Neue" + fontFamily: "Helvetica Neue", + maxRotation: 45, } }; @@ -58,119 +100,100 @@ return this.options.position == "top" || this.options.position == "bottom"; }, parseTime: function(label) { - if (typeof this.options.time.format !== 'string' && this.options.time.format.call) { - return this.options.time.format(label); - } else { - return moment(label, this.options.time.format); + // Date objects + if (typeof label.getMonth === 'function' || typeof label == 'number') { + return moment(label); } + // Moment support + if (label.isValid && label.isValid()) { + return label; + } + // Custom parsing (return an instance of moment) + if (typeof this.options.tick.format !== 'string' && this.options.tick.format.call) { + return this.options.tick.format(label); + } + // Moment format parsing + return moment(label, this.options.tick.format); }, - buildLabels: function(index) { - // Actual labels on the grid - this.labels = []; - // A map of original labelIndex to time labelIndex - this.timeLabelIndexMap = {}; - // The time formatted versions of the labels for use by tooltips - this.data.timeLabels = []; + generateTicks: function(index) { - var definedMoments = []; + this.ticks = []; + this.labelMoments = []; - // Format each point into a moment + // Parse each label into a moment this.data.labels.forEach(function(label, index) { - definedMoments.push(this.parseTime(label)); + var labelMoment = this.parseTime(label); + if (this.options.tick.round) { + labelMoment.startOf(this.options.tick.round); + } + this.labelMoments.push(labelMoment); }, 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(); - // Find or set the unit of time - if (!this.options.time.unit) { - + // Set unit override if applicable + if (this.options.tick.unit) { + this.tickUnit = this.options.tick.unit || 'day'; + this.displayFormat = time.unit.day.display; + this.tickRange = Math.ceil(this.lastTick.diff(this.firstTick, this.tickUnit, true)); + } else { // Determine the smallest needed unit of the time - helpers.each([ - 'millisecond', - 'second', - 'minute', - 'hour', - 'day', - 'week', - 'month', - 'quarter', - 'year', - ], function(format) { - if (this.timeUnit) { + var innerWidth = this.width - (this.paddingLeft + this.paddingRight); + var labelCapacity = innerWidth / this.options.labels.fontSize + 4; + var buffer = this.options.tick.round ? 0 : 2; + + this.tickRange = Math.ceil(this.lastTick.diff(this.firstTick, true) + buffer); + var done; + + helpers.each(time.units, function(format) { + if (this.tickRange <= labelCapacity) { return; } - var start; - helpers.each(definedMoments, function(mom) { - if (!start) { - start = mom[format](); - } + this.tickUnit = format; + this.tickRange = Math.ceil(this.lastTick.diff(this.firstTick, this.tickUnit) + buffer); + this.displayFormat = time.unit[format].display; - if (mom[format]() !== start) { - this.timeUnit = format; - if (!this.displayFormat) { - this.displayFormat = this.options.time.unitFormats[format]; - } - } - }, this); }, this); - } else { - this.timeUnit = this.options.time.unit; } - if (!this.timeUnit) { - this.timeUnit = 'day'; - this.displayFormat = this.options.time.unitFormats.day; + this.firstTick.startOf(this.tickUnit); + this.lastTick.endOf(this.tickUnit); + + + // Tick displayFormat override + if (this.options.tick.displayFormat) { + this.displayFormat = this.options.tick.displayFormat; } - - if (this.options.time.display) { - this.displayFormat = this.options.time.display; - } - - - // Find the first and last moments - this.firstMoment = moment.min.call(this, definedMoments); - this.lastMoment = moment.max.call(this, definedMoments); - - // Find the length of the timeframe in the desired unit - var momentRangeLength = this.lastMoment.diff(this.firstMoment, this.timeUnit); - - helpers.each(definedMoments, function(definedMoment, index) { - this.timeLabelIndexMap[index] = momentRangeLength - this.lastMoment.diff(definedMoment, this.timeUnit); - this.data.timeLabels.push( - definedMoment - .format(this.options.time.display ? this.options.time.display : this.displayFormat) - ); - }, this); - // For every unit in between the first and last moment, create a moment and add it to the labels tick var i = 0; if (this.options.labels.userCallback) { - for (; i <= momentRangeLength; i++) { - this.labels.push( - this.options.labels.userCallback(this.firstMoment - .add((!i ? 0 : 1), this.timeUnit) - .format(this.options.time.display ? this.options.time.display : this.displayFormat) + for (; i <= this.tickRange; i++) { + this.ticks.push( + this.options.labels.userCallback(this.firstTick.clone() + .add(i, this.tickUnit) + .format(this.options.tick.displayFormat ? this.options.tick.displayFormat : time.unit[this.tickUnit].display) ) ); } } else { - for (; i <= momentRangeLength; i++) { - this.labels.push(this.firstMoment - .add((!i ? 0 : 1), this.timeUnit) - .format(this.options.time.display ? this.options.time.display : this.displayFormat) + for (; i <= this.tickRange; i++) { + this.ticks.push(this.firstTick.clone() + .add(i, this.tickUnit) + .format(this.options.tick.displayFormat ? this.options.tick.displayFormat : time.unit[this.tickUnit].display) ); } } - - }, - getPixelForValue: function(value, index, datasetIndex, includeOffset) { + getPixelForValue: function(value, decimal, datasetIndex, includeOffset) { // This must be called after fit has been run so that // this.left, this.top, this.right, and this.bottom have been defined if (this.isHorizontal()) { var innerWidth = this.width - (this.paddingLeft + this.paddingRight); - var valueWidth = innerWidth / Math.max((this.labels.length - ((this.options.gridLines.offsetGridLines) ? 0 : 1)), 1); - var valueOffset = (valueWidth * index) + this.paddingLeft; + var valueWidth = innerWidth / Math.max((this.ticks.length - ((this.options.gridLines.offsetGridLines) ? 0 : 1)), 1); + var valueOffset = (innerWidth * decimal) + this.paddingLeft; if (this.options.gridLines.offsetGridLines && includeOffset) { valueOffset += (valueWidth / 2); @@ -178,17 +201,18 @@ return this.left + Math.round(valueOffset); } else { - return this.top + (index * (this.height / this.labels.length)); + return this.top + (decimal * (this.height / this.ticks.length)); } }, getPointPixelForValue: function(value, index, datasetIndex) { - // This function references the timeLaabelIndexMap to know which index in the timeLabels corresponds to the index of original labels - return this.getPixelForValue(value, this.timeLabelIndexMap[index], datasetIndex, true); + + var offset = this.labelMoments[index].diff(this.firstTick, this.tickUnit, true); + return this.getPixelForValue(value, offset / this.tickRange, datasetIndex); }, // Functions needed for bar charts calculateBaseWidth: function() { - return (this.getPixelForValue(null, 1, 0, true) - this.getPixelForValue(null, 0, 0, true)) - (2 * this.options.categorySpacing); + return (this.getPixelForValue(null, this.ticks.length / 100, 0, true) - this.getPixelForValue(null, 0, 0, true)) - (2 * this.options.categorySpacing); }, calculateBarWidth: function(barDatasetCount) { //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset @@ -200,6 +224,7 @@ return (baseWidth / barDatasetCount); }, calculateBarX: function(barDatasetCount, datasetIndex, elementIndex) { + var xWidth = this.calculateBaseWidth(), xAbsolute = this.getPixelForValue(null, elementIndex, datasetIndex, true) - (xWidth / 2), barWidth = this.calculateBarWidth(barDatasetCount); @@ -211,14 +236,14 @@ return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * this.options.spacing) + barWidth / 2; }, - calculateLabelRotation: function(maxHeight, margins) { + calculateTickRotation: function(maxHeight, margins) { //Get the width of each grid by calculating the difference //between x offsets between 0 and 1. var labelFont = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily); this.ctx.font = labelFont; - var firstWidth = this.ctx.measureText(this.labels[0]).width; - var lastWidth = this.ctx.measureText(this.labels[this.labels.length - 1]).width; + var firstWidth = this.ctx.measureText(this.ticks[0]).width; + var lastWidth = this.ctx.measureText(this.ticks[this.ticks.length - 1]).width; var firstRotated; var lastRotated; @@ -228,7 +253,7 @@ this.labelRotation = 0; if (this.options.display) { - var originalLabelWidth = helpers.longestText(this.ctx, labelFont, this.labels); + var originalLabelWidth = helpers.longestText(this.ctx, labelFont, this.ticks); var cosRotation; var sinRotation; var firstRotatedWidth; @@ -238,7 +263,7 @@ //Allow 3 pixels x2 padding either side for label readability // only the index matters for a dataset scale, but we want a consistent interface between scales - var datasetWidth = Math.floor(this.getPixelForValue(0, 1) - this.getPixelForValue(0, 0)) - 6; + var datasetWidth = Math.floor(this.getPixelForValue(null, 1 / this.ticks.length) - this.getPixelForValue(null, 0)) - 6; //Max label rotation can be set or default to 90 - also act as a loop counter while (this.labelWidth > datasetWidth && this.labelRotation <= this.options.labels.maxRotation) { @@ -264,6 +289,7 @@ this.labelRotation++; this.labelWidth = cosRotation * originalLabelWidth; + } } else { this.labelWidth = 0; @@ -292,8 +318,8 @@ this.height = maxHeight; } - this.buildLabels(); - this.calculateLabelRotation(maxHeight, margins); + this.generateTicks(); + this.calculateTickRotation(maxHeight, margins); var minSize = { width: 0, @@ -301,7 +327,7 @@ }; var labelFont = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily); - var longestLabelWidth = helpers.longestText(this.ctx, labelFont, this.labels); + var longestLabelWidth = helpers.longestText(this.ctx, labelFont, this.ticks); // Width if (this.isHorizontal()) { @@ -340,17 +366,17 @@ var isRotated = this.labelRotation !== 0; var skipRatio = false; - if ((this.options.labels.fontSize + 4) * this.labels.length > (this.width - (this.paddingLeft + this.paddingRight))) { - skipRatio = 1 + Math.floor(((this.options.labels.fontSize + 4) * this.labels.length) / (this.width - (this.paddingLeft + this.paddingRight))); + if ((this.options.labels.fontSize + 4) * this.ticks.length > (this.width - (this.paddingLeft + this.paddingRight))) { + skipRatio = 1 + Math.floor(((this.options.labels.fontSize + 4) * this.ticks.length) / (this.width - (this.paddingLeft + this.paddingRight))); } - helpers.each(this.labels, function(label, index) { - // Blank labels - if ((skipRatio > 1 && index % skipRatio > 0) || (label === undefined || label === null)) { + helpers.each(this.ticks, function(tick, index) { + // Blank ticks + if ((skipRatio > 1 && index % skipRatio > 0) || (tick === undefined || tick === null)) { return; } - var xLineValue = this.getPixelForValue(label, index, null, false); // xvalues for grid lines - var xLabelValue = this.getPixelForValue(label, index, null, true); // x values for labels (need to consider offsetLabel option) + var xLineValue = this.getPixelForValue(null, (1 / (this.ticks.length - 1)) * index, null, false); // xvalues for grid lines + var xLabelValue = this.getPixelForValue(null, (1 / (this.ticks.length - 1)) * index, null, true); // x values for ticks (need to consider offsetLabel option) if (this.options.gridLines.show) { if (index === 0) { @@ -366,7 +392,7 @@ xLineValue += helpers.aliasPixel(this.ctx.lineWidth); - // Draw the label area + // Draw the tick area this.ctx.beginPath(); if (this.options.gridLines.drawTicks) { @@ -391,7 +417,7 @@ this.ctx.font = this.font; this.ctx.textAlign = (isRotated) ? "right" : "center"; this.ctx.textBaseline = (isRotated) ? "middle" : "top"; - this.ctx.fillText(label, 0, 0); + this.ctx.fillText(tick, 0, 0); this.ctx.restore(); } }, this); From 5ff2b530b9927b48c68965e8ac568ab2719cf5db Mon Sep 17 00:00:00 2001 From: Menno Dekker Date: Thu, 17 Sep 2015 09:43:02 +0200 Subject: [PATCH 12/14] Hover on dataset now works just like hover on label Fixed some errors in de hover on dataset mode, only tested on bar charts --- src/core/core.controller.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 5b611e8ea..3833cd153 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -328,10 +328,12 @@ var eventPosition = helpers.getRelativePosition(e); var elementsArray = []; - for (var datasetIndex = 0; datasetIndex < this.chart.data.datasets.length; datasetIndex++) { - for (elementIndex = 0; elementIndex < this.chart.data.datasets[datasetIndex].metaData.length; elementIndex++) { - if (this.chart.data.datasets[datasetIndex].metaData[elementIndex].inLabelRange(eventPosition.x, eventPosition.y)) { - helpers.each(this.chart.data.datasets, datasetIterator); + for (var datasetIndex = 0; datasetIndex < this.data.datasets.length; datasetIndex++) { + for (var elementIndex = 0; elementIndex < this.data.datasets[datasetIndex].metaData.length; elementIndex++) { + if (this.data.datasets[datasetIndex].metaData[elementIndex].inLabelRange(eventPosition.x, eventPosition.y)) { + helpers.each(this.data.datasets[datasetIndex].metaData, function(element, index) { + elementsArray.push(this.data.datasets[datasetIndex].metaData); + }, this); } } } @@ -422,12 +424,11 @@ this.data.datasets[this.lastActive[0]._datasetIndex].controller.removeHoverStyle(this.lastActive[0], this.lastActive[0]._datasetIndex, this.lastActive[0]._index); break; case 'label': + case 'dataset': for (var i = 0; i < this.lastActive.length; i++) { this.data.datasets[this.lastActive[i]._datasetIndex].controller.removeHoverStyle(this.lastActive[i], this.lastActive[i]._datasetIndex, this.lastActive[i]._index); } break; - case 'dataset': - break; default: // Don't change anything } @@ -440,12 +441,11 @@ this.data.datasets[this.active[0]._datasetIndex].controller.setHoverStyle(this.active[0]); break; case 'label': + case 'dataset': for (var i = 0; i < this.active.length; i++) { this.data.datasets[this.active[i]._datasetIndex].controller.setHoverStyle(this.active[i]); } break; - case 'dataset': - break; default: // Don't change anything } From 5047678799796f6aeb07a19777d66e054e7e14f4 Mon Sep 17 00:00:00 2001 From: Menno Dekker Date: Thu, 17 Sep 2015 10:08:42 +0200 Subject: [PATCH 13/14] Tabs to spaces + fixed error --- src/core/core.controller.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/core.controller.js b/src/core/core.controller.js index 3833cd153..806248485 100644 --- a/src/core/core.controller.js +++ b/src/core/core.controller.js @@ -331,9 +331,9 @@ for (var datasetIndex = 0; datasetIndex < this.data.datasets.length; datasetIndex++) { for (var elementIndex = 0; elementIndex < this.data.datasets[datasetIndex].metaData.length; elementIndex++) { if (this.data.datasets[datasetIndex].metaData[elementIndex].inLabelRange(eventPosition.x, eventPosition.y)) { - helpers.each(this.data.datasets[datasetIndex].metaData, function(element, index) { - elementsArray.push(this.data.datasets[datasetIndex].metaData); - }, this); + helpers.each(this.data.datasets[datasetIndex].metaData, function(element, index) { + elementsArray.push(element); + }, this); } } } From 723c85320ff2d3045e33a518eca5ba3ae45e7b72 Mon Sep 17 00:00:00 2001 From: Evert Timberg Date: Thu, 17 Sep 2015 20:29:41 -0400 Subject: [PATCH 14/14] Unit tests for logarithmic scale. Fixed some issues in stacked mode since it's not possible for a 0 crossing to occur with a log scale. --- src/scales/scale.logarithmic.js | 45 +- test/scale.logarithmic.tests.js | 1561 +++++++++++++++++++++++++++++++ 2 files changed, 1577 insertions(+), 29 deletions(-) create mode 100644 test/scale.logarithmic.tests.js diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 7d0f181c9..20545846e 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -22,7 +22,6 @@ // scale numbers reverse: false, - beginAtZero: false, override: null, // label settings @@ -160,8 +159,7 @@ this.min = null; this.max = null; - var positiveValues = []; - var negativeValues = []; + var values = []; if (this.options.stacked) { helpers.each(this.data.datasets, function(dataset) { @@ -170,23 +168,18 @@ var value = this.getRightValue(rawValue); - positiveValues[index] = positiveValues[index] || 0; - negativeValues[index] = negativeValues[index] || 0; + values[index] = values[index] || 0; if (this.options.relativePoints) { - positiveValues[index] = 100; + values[index] = 100; } else { - if (value < 0) { - negativeValues[index] += value; - } else { - positiveValues[index] += value; - } + // Don't need to split positive and negative since the log scale can't handle a 0 crossing + values[index] += value; } }, this); } }, this); - var values = positiveValues.concat(negativeValues); this.min = helpers.min(values); this.max = helpers.max(values); @@ -213,8 +206,13 @@ } if (this.min === this.max) { - this.min--; - this.max++; + if (this.min !== 0 && this.min !== null) { + this.min = Math.pow(10, Math.floor(helpers.log10(this.min)) - 1); + this.max = Math.pow(10, Math.floor(helpers.log10(this.max)) + 1); + } else { + this.min = 1; + this.max = 10; + } } }, @@ -251,17 +249,9 @@ var value = this.data.datasets[datasetIndex].data[index]; - if (value < 0) { - for (var i = 0; i < datasetIndex; i++) { - if (this.data.datasets[i].yAxisID === this.id) { - base += this.data.datasets[i].data[index] < 0 ? this.data.datasets[i].data[index] : 0; - } - } - } else { - for (var j = 0; j < datasetIndex; j++) { - if (this.data.datasets[j].yAxisID === this.id) { - base += this.data.datasets[j].data[index] > 0 ? this.data.datasets[j].data[index] : 0; - } + for (var j = 0; j < datasetIndex; j++) { + if (this.data.datasets[j].yAxisID === this.id) { + base += this.data.datasets[j].data[index]; } } @@ -270,10 +260,7 @@ base = this.getPixelForValue(this.min); - if (this.beginAtZero || ((this.min <= 0 && this.max >= 0) || (this.min >= 0 && this.max <= 0))) { - base = this.getPixelForValue(0); - base += this.options.gridLines.lineWidth; - } else if (this.min < 0 && this.max < 0) { + if (this.min < 0 && this.max < 0) { // All values are negative. Use the top as the base base = this.getPixelForValue(this.max); } diff --git a/test/scale.logarithmic.tests.js b/test/scale.logarithmic.tests.js new file mode 100644 index 000000000..7a472d88c --- /dev/null +++ b/test/scale.logarithmic.tests.js @@ -0,0 +1,1561 @@ +describe('Logarithmic Scale tests', function() { + + it('Should register the constructor with the scale service', function() { + var Constructor = Chart.scaleService.getScaleConstructor('logarithmic'); + expect(Constructor).not.toBe(undefined); + expect(typeof Constructor).toBe('function'); + }); + + it('Should have the correct default config', function() { + var defaultConfig = Chart.scaleService.getScaleDefaults('logarithmic'); + expect(defaultConfig).toEqual({ + display: true, + position: "left", + gridLines: { + show: true, + color: "rgba(0, 0, 0, 0.1)", + lineWidth: 1, + drawOnChartArea: true, + drawTicks: true, + zeroLineWidth: 1, + zeroLineColor: "rgba(0,0,0,0.25)", + }, + reverse: false, + override: null, + labels: { + show: true, + mirror: false, + padding: 10, + template: "<%var remain = value / (Math.pow(10, Math.floor(Chart.helpers.log10(value))));if (remain === 1 || remain === 2 || remain === 5) {%><%=value.toExponential()%><%} else {%><%= null %><%}%>", + fontSize: 12, + fontStyle: "normal", + fontColor: "#666", + fontFamily: "Helvetica Neue" + } + }); + }); + + it('Should correctly determine the max & min data values', function() { + var scaleID = 'myScale'; + + var mockData = { + datasets: [{ + yAxisID: scaleID, + data: [10, 5, 5000, 78, 450] + }, { + yAxisID: 'second scale', + data: [1, 1000, 10, 100], + }, { + yAxisID: scaleID, + data: [150] + }] + }; + + var Constructor = Chart.scaleService.getScaleConstructor('logarithmic'); + var scale = new Constructor({ + ctx: {}, + options: Chart.scaleService.getScaleDefaults('logarithmic'), // use default config for scale + data: mockData, + id: scaleID + }); + + expect(scale).not.toEqual(undefined); // must construct + expect(scale.min).toBe(undefined); // not yet set + expect(scale.max).toBe(undefined); + + scale.calculateRange(); + expect(scale.min).toBe(5); + expect(scale.max).toBe(5000); + }); + + it('Should correctly determine the max & min for scatter data', function() { + var scaleID = 'myScale'; + + var mockData = { + datasets: [{ + xAxisID: scaleID, // for the horizontal scale + yAxisID: scaleID, + data: [{ + x: 10, + y: 100 + }, { + x: 2, + y: 6 + }, { + x: 65, + y: 121 + }, { + x: 99, + y: 7 + }] + }] + }; + + var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('logarithmic')); + var Constructor = Chart.scaleService.getScaleConstructor('logarithmic'); + var verticalScale = new Constructor({ + ctx: {}, + options: config, + data: mockData, + id: scaleID + }); + + verticalScale.calculateRange(); + expect(verticalScale.min).toBe(6); + expect(verticalScale.max).toBe(121); + + var horizontalConfig = Chart.helpers.clone(config); + horizontalConfig.position = 'bottom'; + var horizontalScale = new Constructor({ + ctx: {}, + options: horizontalConfig, + data: mockData, + id: scaleID, + }); + + horizontalScale.calculateRange(); + expect(horizontalScale.min).toBe(2); + expect(horizontalScale.max).toBe(99); + }); + + it('Should correctly determine the min and max data values when stacked mode is turned on', function() { + var scaleID = 'myScale'; + + var mockData = { + datasets: [{ + yAxisID: scaleID, + data: [10, 5, 1, 5, 78, 100] + }, { + yAxisID: 'second scale', + data: [-1000, 1000], + }, { + yAxisID: scaleID, + data: [150, 10, 10, 100, 10, 9] + }] + }; + + var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('logarithmic')); + config.stacked = true; // enable scale stacked mode + + var Constructor = Chart.scaleService.getScaleConstructor('logarithmic'); + var scale = new Constructor({ + ctx: {}, + options: config, + data: mockData, + id: scaleID + }); + + scale.calculateRange(); + expect(scale.min).toBe(11); + expect(scale.max).toBe(160); + }); + + it('Should ensure that the scale has a max and min that are not equal', function() { + var scaleID = 'myScale'; + + var mockData = { + datasets: [] + }; + + var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('logarithmic')); + var Constructor = Chart.scaleService.getScaleConstructor('logarithmic'); + var scale = new Constructor({ + ctx: {}, + options: config, + data: mockData, + id: scaleID + }); + + scale.calculateRange(); + expect(scale.min).toBe(1); + expect(scale.max).toBe(10); + + mockData.datasets = [{ + yAxisID: scaleID, + data: [0.15, 0.15] + }]; + + scale.calculateRange(); + expect(scale.min).toBe(0.01); + expect(scale.max).toBe(1); + }); + + it('Should generate tick marks', function() { + var scaleID = 'myScale'; + + var mockData = { + datasets: [{ + yAxisID: scaleID, + data: [10, 5, 1, 25, 78] + }, ] + }; + + var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('logarithmic')); + var Constructor = Chart.scaleService.getScaleConstructor('logarithmic'); + var scale = new Constructor({ + ctx: {}, + options: config, + data: mockData, + id: scaleID + }); + + scale.calculateRange(); + expect(scale.ticks).toBe(undefined); // not set + + // Large enough to be unimportant + var maxWidth = 400; + var maxHeight = 400; + scale.generateTicks(maxWidth, maxHeight); + + // Counts down because the lines are drawn top to bottom + expect(scale.ticks).toEqual([100, 90, 80, 70, 60, 50, 40, 30, 20, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]); + expect(scale.start).toBe(1); + expect(scale.end).toBe(100); + }); + + it('Should generate tick marks in the correct order in reversed mode', function() { + var scaleID = 'myScale'; + + var mockData = { + datasets: [{ + yAxisID: scaleID, + data: [10, 5, 1, 25, 78] + }, ] + }; + + var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('logarithmic')); + config.reverse = true; + + var Constructor = Chart.scaleService.getScaleConstructor('logarithmic'); + var scale = new Constructor({ + ctx: {}, + options: config, + data: mockData, + id: scaleID + }); + + scale.calculateRange(); + expect(scale.ticks).toBe(undefined); // not set + + // Large enough to be unimportant + var maxWidth = 400; + var maxHeight = 400; + scale.generateTicks(maxWidth, maxHeight); + + // Counts down because the lines are drawn top to bottom + expect(scale.ticks).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]); + expect(scale.start).toBe(100); + expect(scale.end).toBe(1); + }); + + it('Should generate tick marks using the user supplied options', function() { + var scaleID = 'myScale'; + + var mockData = { + datasets: [{ + yAxisID: scaleID, + data: [10, 5, 0, 25, 78] + }, ] + }; + + var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('logarithmic')); + config.override = { + steps: 9, + start: 1, + stepWidth: 1 + }; + + var Constructor = Chart.scaleService.getScaleConstructor('logarithmic'); + var scale = new Constructor({ + ctx: {}, + options: config, + data: mockData, + id: scaleID + }); + + scale.calculateRange(); + + // Large enough to be unimportant + var maxWidth = 400; + var maxHeight = 400; + scale.generateTicks(maxWidth, maxHeight); + + expect(scale.ticks).toEqual([10, 9, 8, 7, 6, 5, 4, 3, 2, 1]); + expect(scale.start).toBe(1); + expect(scale.end).toBe(10); + }); + + it('Should build labels using the default template', function() { + var scaleID = 'myScale'; + + var mockData = { + datasets: [{ + yAxisID: scaleID, + data: [10, 5, 1, 25, 78] + }, ] + }; + + var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('logarithmic')); + var Constructor = Chart.scaleService.getScaleConstructor('logarithmic'); + var scale = new Constructor({ + ctx: {}, + options: config, + data: mockData, + id: scaleID + }); + + scale.calculateRange(); + + // Large enough to be unimportant + var maxWidth = 400; + var maxHeight = 400; + scale.generateTicks(maxWidth, maxHeight); + + // Generate labels + scale.buildLabels(); + + expect(scale.labels).toEqual(['1e+2', '', '', '', '', '5e+1', '', '', '2e+1', '1e+1', '', '', '', '', '5e+0', '', '', '2e+0', '1e+0']); + }); + + it('Should build labels using the user supplied callback', function() { + var scaleID = 'myScale'; + + var mockData = { + datasets: [{ + yAxisID: scaleID, + data: [10, 5, 1, 25, 78] + }, ] + }; + + var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('logarithmic')); + config.labels.userCallback = function(value, index) { + return index.toString(); + }; + + var Constructor = Chart.scaleService.getScaleConstructor('logarithmic'); + var scale = new Constructor({ + ctx: {}, + options: config, + data: mockData, + id: scaleID + }); + + scale.calculateRange(); + + // Large enough to be unimportant + var maxWidth = 400; + var maxHeight = 400; + scale.generateTicks(maxWidth, maxHeight); + + // Generate labels + scale.buildLabels(); + + // Just the index + expect(scale.labels).toEqual(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18']); + }); + + it('Should get the correct pixel value for a point', function() { + var scaleID = 'myScale'; + + var mockData = { + datasets: [{ + xAxisID: scaleID, // for the horizontal scale + yAxisID: scaleID, + data: [10, 5, 1, 25, 78] + }] + }; + + var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('logarithmic')); + var Constructor = Chart.scaleService.getScaleConstructor('logarithmic'); + var verticalScale = new Constructor({ + ctx: {}, + options: config, + data: mockData, + id: scaleID + }); + + verticalScale.calculateRange(); + verticalScale.generateTicks(50, 100); + + // Fake out positioning of the scale service + verticalScale.left = 0; + verticalScale.top = 0; + verticalScale.right = 50; + verticalScale.bottom = 110; + verticalScale.paddingTop = 5; + verticalScale.paddingBottom = 5; + verticalScale.width = 50; + verticalScale.height = 110; + + expect(verticalScale.getPointPixelForValue(100, 0, 0)).toBe(5); // top + paddingTop + expect(verticalScale.getPointPixelForValue(1, 0, 0)).toBe(105); // bottom - paddingBottom + expect(verticalScale.getPointPixelForValue(10, 0, 0)).toBe(55); // halfway + + var horizontalConfig = Chart.helpers.clone(config); + horizontalConfig.position = 'bottom'; + var horizontalScale = new Constructor({ + ctx: {}, + options: horizontalConfig, + data: mockData, + id: scaleID, + }); + + horizontalScale.calculateRange(); + horizontalScale.generateTicks(100, 50); + + // Fake out positioning of the scale service + horizontalScale.left = 0; + horizontalScale.top = 0; + horizontalScale.right = 110; + horizontalScale.bottom = 50; + horizontalScale.paddingLeft = 5; + horizontalScale.paddingRight = 5; + horizontalScale.width = 110; + horizontalScale.height = 50; + + // Range expands to [-2, 2] due to nicenum algorithm + expect(horizontalScale.getPointPixelForValue(100, 0, 0)).toBe(105); // right - paddingRight + expect(horizontalScale.getPointPixelForValue(1, 0, 0)).toBe(5); // left + paddingLeft + expect(horizontalScale.getPointPixelForValue(10, 0, 0)).toBe(55); // halfway + }); + + it('should get the correct pixel value for a bar', function() { + var scaleID = 'myScale'; + + var mockData = { + datasets: [{ + xAxisID: scaleID, // for the horizontal scale + yAxisID: scaleID, + data: [10, 5, 1, 25, 78] + }] + }; + + var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('logarithmic')); + var Constructor = Chart.scaleService.getScaleConstructor('logarithmic'); + var verticalScale = new Constructor({ + ctx: {}, + options: config, + data: mockData, + id: scaleID + }); + + verticalScale.calculateRange(); + verticalScale.generateTicks(50, 100); + + // Fake out positioning of the scale service + verticalScale.left = 0; + verticalScale.top = 0; + verticalScale.right = 50; + verticalScale.bottom = 110; + verticalScale.paddingTop = 5; + verticalScale.paddingBottom = 5; + verticalScale.width = 50; + verticalScale.height = 110; + + expect(verticalScale.calculateBarBase()).toBe(105); // bottom + expect(verticalScale.calculateBarY(0, 3)).toBe(35.10299956639811); // bottom + }); + + it('should fit correctly', function() { + var scaleID = 'myScale'; + + var mockData = { + datasets: [{ + xAxisID: scaleID, // for the horizontal scale + yAxisID: scaleID, + data: [10, 5, 1, 25, 78] + }] + }; + var mockContext = window.createMockContext(); + + var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('logarithmic')); + var Constructor = Chart.scaleService.getScaleConstructor('logarithmic'); + var verticalScale = new Constructor({ + ctx: mockContext, + options: config, + data: mockData, + id: scaleID + }); + + var minSize = verticalScale.fit(100, 300); + expect(minSize).toEqual({ + width: 53, + height: 300, + }); + expect(verticalScale.width).toBe(53); + expect(verticalScale.height).toBe(300); + expect(verticalScale.paddingTop).toBe(6); + expect(verticalScale.paddingBottom).toBe(6); + expect(verticalScale.paddingLeft).toBe(0); + expect(verticalScale.paddingRight).toBe(0); + + // Refit with margins to see the padding go away + minSize = verticalScale.fit(53, 300, { + left: 0, + right: 0, + top: 15, + bottom: 3 + }); + expect(minSize).toEqual({ + width: 53, + height: 300, + }); + expect(verticalScale.paddingTop).toBe(0); + expect(verticalScale.paddingBottom).toBe(3); + expect(verticalScale.paddingLeft).toBe(0); + expect(verticalScale.paddingRight).toBe(0); + }); + + it('should fit correctly when horizontal', function() { + var scaleID = 'myScale'; + + var mockData = { + datasets: [{ + xAxisID: scaleID, // for the horizontal scale + yAxisID: scaleID, + data: [10, 5, 1, 25, 78] + }] + }; + var mockContext = window.createMockContext(); + + var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('logarithmic')); + config.position = "bottom"; + var Constructor = Chart.scaleService.getScaleConstructor('logarithmic'); + var verticalScale = new Constructor({ + ctx: mockContext, + options: config, + data: mockData, + id: scaleID + }); + + var minSize = verticalScale.fit(100, 300); + expect(minSize).toEqual({ + width: 100, + height: 28, + }); + expect(verticalScale.width).toBe(100); + expect(verticalScale.height).toBe(28); + expect(verticalScale.paddingTop).toBe(0); + expect(verticalScale.paddingBottom).toBe(0); + expect(verticalScale.paddingLeft).toBe(20); + expect(verticalScale.paddingRight).toBe(20); + + // Refit with margins to see the padding go away + minSize = verticalScale.fit(100, 28, { + left: 10, + right: 6, + top: 15, + bottom: 3 + }); + expect(minSize).toEqual({ + width: 100, + height: 28, + }); + expect(verticalScale.paddingTop).toBe(0); + expect(verticalScale.paddingBottom).toBe(0); + expect(verticalScale.paddingLeft).toBe(10); + expect(verticalScale.paddingRight).toBe(14); + }); + + it('should draw correctly horizontally', function() { + var scaleID = 'myScale'; + + var mockData = { + datasets: [{ + xAxisID: scaleID, // for the horizontal scale + yAxisID: scaleID, + data: [10, 5, 1, 25, 78] + }] + }; + var mockContext = window.createMockContext(); + + var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('logarithmic')); + config.position = "bottom"; + var Constructor = Chart.scaleService.getScaleConstructor('logarithmic'); + var horizontalScale = new Constructor({ + ctx: mockContext, + options: config, + data: mockData, + id: scaleID + }); + + var minSize = horizontalScale.fit(100, 300); + minSize = horizontalScale.fit(100, 28, { + left: 10, + right: 6, + top: 15, + bottom: 3 + }); + + horizontalScale.left = 0; + horizontalScale.right = minSize.width; + horizontalScale.top = 0; + horizontalScale.bottom = minSize.height; + + var chartArea = { + top: 100, + bottom: 0, + left: 0, + right: minSize.width + }; + horizontalScale.draw(chartArea); + + expect(mockContext.getCalls()).toEqual([{ + "name": "measureText", + "args": ["1e+0"] + }, { + "name": "measureText", + "args": ["1e+2"] + }, { + "name": "measureText", + "args": ["1e+0"] + }, { + "name": "measureText", + "args": ["1e+2"] + }, { + "name": "setFillStyle", + "args": ["#666"] + }, { + "name": "setLineWidth", + "args": [1] + }, { + "name": "setStrokeStyle", + "args": ["rgba(0,0,0,0.25)"] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [10.5, 0] + }, { + "name": "lineTo", + "args": [10.5, 5] + }, { + "name": "moveTo", + "args": [10.5, 100] + }, { + "name": "lineTo", + "args": [10.5, 0] + }, { + "name": "stroke", + "args": [] + }, { + "name": "setLineWidth", + "args": [1] + }, { + "name": "setStrokeStyle", + "args": ["rgba(0, 0, 0, 0.1)"] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [21.939139835231288, 0] + }, { + "name": "lineTo", + "args": [21.939139835231288, 5] + }, { + "name": "moveTo", + "args": [21.939139835231288, 100] + }, { + "name": "lineTo", + "args": [21.939139835231288, 0] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [28.63060767934717, 0] + }, { + "name": "lineTo", + "args": [28.63060767934717, 5] + }, { + "name": "moveTo", + "args": [28.63060767934717, 100] + }, { + "name": "lineTo", + "args": [28.63060767934717, 0] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [33.378279670462575, 0] + }, { + "name": "lineTo", + "args": [33.378279670462575, 5] + }, { + "name": "moveTo", + "args": [33.378279670462575, 100] + }, { + "name": "lineTo", + "args": [33.378279670462575, 0] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [37.06086016476871, 0] + }, { + "name": "lineTo", + "args": [37.06086016476871, 5] + }, { + "name": "moveTo", + "args": [37.06086016476871, 100] + }, { + "name": "lineTo", + "args": [37.06086016476871, 0] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [40.06974751457846, 0] + }, { + "name": "lineTo", + "args": [40.06974751457846, 5] + }, { + "name": "moveTo", + "args": [40.06974751457846, 100] + }, { + "name": "lineTo", + "args": [40.06974751457846, 0] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [42.613725520541756, 0] + }, { + "name": "lineTo", + "args": [42.613725520541756, 5] + }, { + "name": "moveTo", + "args": [42.613725520541756, 100] + }, { + "name": "lineTo", + "args": [42.613725520541756, 0] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [44.817419505693856, 0] + }, { + "name": "lineTo", + "args": [44.817419505693856, 5] + }, { + "name": "moveTo", + "args": [44.817419505693856, 100] + }, { + "name": "lineTo", + "args": [44.817419505693856, 0] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [46.76121535869434, 0] + }, { + "name": "lineTo", + "args": [46.76121535869434, 5] + }, { + "name": "moveTo", + "args": [46.76121535869434, 100] + }, { + "name": "lineTo", + "args": [46.76121535869434, 0] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [48.5, 0] + }, { + "name": "lineTo", + "args": [48.5, 5] + }, { + "name": "moveTo", + "args": [48.5, 100] + }, { + "name": "lineTo", + "args": [48.5, 0] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [59.93913983523129, 0] + }, { + "name": "lineTo", + "args": [59.93913983523129, 5] + }, { + "name": "moveTo", + "args": [59.93913983523129, 100] + }, { + "name": "lineTo", + "args": [59.93913983523129, 0] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [66.63060767934718, 0] + }, { + "name": "lineTo", + "args": [66.63060767934718, 5] + }, { + "name": "moveTo", + "args": [66.63060767934718, 100] + }, { + "name": "lineTo", + "args": [66.63060767934718, 0] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [71.37827967046258, 0] + }, { + "name": "lineTo", + "args": [71.37827967046258, 5] + }, { + "name": "moveTo", + "args": [71.37827967046258, 100] + }, { + "name": "lineTo", + "args": [71.37827967046258, 0] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [75.06086016476871, 0] + }, { + "name": "lineTo", + "args": [75.06086016476871, 5] + }, { + "name": "moveTo", + "args": [75.06086016476871, 100] + }, { + "name": "lineTo", + "args": [75.06086016476871, 0] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [78.06974751457845, 0] + }, { + "name": "lineTo", + "args": [78.06974751457845, 5] + }, { + "name": "moveTo", + "args": [78.06974751457845, 100] + }, { + "name": "lineTo", + "args": [78.06974751457845, 0] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [80.61372552054176, 0] + }, { + "name": "lineTo", + "args": [80.61372552054176, 5] + }, { + "name": "moveTo", + "args": [80.61372552054176, 100] + }, { + "name": "lineTo", + "args": [80.61372552054176, 0] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [82.81741950569385, 0] + }, { + "name": "lineTo", + "args": [82.81741950569385, 5] + }, { + "name": "moveTo", + "args": [82.81741950569385, 100] + }, { + "name": "lineTo", + "args": [82.81741950569385, 0] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [84.76121535869434, 0] + }, { + "name": "lineTo", + "args": [84.76121535869434, 5] + }, { + "name": "moveTo", + "args": [84.76121535869434, 100] + }, { + "name": "lineTo", + "args": [84.76121535869434, 0] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [86.5, 0] + }, { + "name": "lineTo", + "args": [86.5, 5] + }, { + "name": "moveTo", + "args": [86.5, 100] + }, { + "name": "lineTo", + "args": [86.5, 0] + }, { + "name": "stroke", + "args": [] + }, { + "name": "fillText", + "args": ["1e+0", 10, 10] + }, { + "name": "fillText", + "args": ["2e+0", 21.439139835231288, 10] + }, { + "name": "fillText", + "args": ["5e+0", 36.56086016476871, 10] + }, { + "name": "fillText", + "args": ["1e+1", 48, 10] + }, { + "name": "fillText", + "args": ["2e+1", 59.43913983523129, 10] + }, { + "name": "fillText", + "args": ["5e+1", 74.56086016476871, 10] + }, { + "name": "fillText", + "args": ["1e+2", 86, 10] + }]); + + // Turn off some drawing + config.gridLines.drawTicks = false; + config.gridLines.drawOnChartArea = false; + config.labels.show = false; + + mockContext.resetCalls(); + + horizontalScale.draw(); + expect(mockContext.getCalls()).toEqual([{ + "name": "setFillStyle", + "args": ["#666"] + }, { + "name": "setLineWidth", + "args": [1] + }, { + "name": "setStrokeStyle", + "args": ["rgba(0,0,0,0.25)"] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }, { + "name": "setLineWidth", + "args": [1] + }, { + "name": "setStrokeStyle", + "args": ["rgba(0, 0, 0, 0.1)"] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }]); + + // Turn off display + + mockContext.resetCalls(); + config.display = false; + horizontalScale.draw(); + expect(mockContext.getCalls()).toEqual([]); + }); + + it('should draw correctly vertically', function() { + var scaleID = 'myScale'; + + var mockData = { + datasets: [{ + xAxisID: scaleID, // for the horizontal scale + yAxisID: scaleID, + data: [10, 5, 1, 2.5, 7.8] + }] + }; + var mockContext = window.createMockContext(); + + var config = Chart.helpers.clone(Chart.scaleService.getScaleDefaults('logarithmic')); + var Constructor = Chart.scaleService.getScaleConstructor('logarithmic'); + var verticalScale = new Constructor({ + ctx: mockContext, + options: config, + data: mockData, + id: scaleID + }); + + var minSize = verticalScale.fit(100, 300); + minSize = verticalScale.fit(33, 300, { + left: 0, + right: 0, + top: 15, + bottom: 3 + }); + expect(minSize).toEqual({ + width: 33, + height: 300, + }); + + verticalScale.left = 0; + verticalScale.right = minSize.width; + verticalScale.top = 0; + verticalScale.bottom = minSize.height; + + var chartArea = { + top: 0, + bottom: minSize.height, + left: minSize.width, + right: minSize.width + 100 + }; + verticalScale.draw(chartArea); + + var expected = [{ + "name": "measureText", + "args": ["1e+1"] + }, { + "name": "measureText", + "args": [""] + }, { + "name": "measureText", + "args": [""] + }, { + "name": "measureText", + "args": [""] + }, { + "name": "measureText", + "args": [""] + }, { + "name": "measureText", + "args": ["5e+0"] + }, { + "name": "measureText", + "args": [""] + }, { + "name": "measureText", + "args": [""] + }, { + "name": "measureText", + "args": ["2e+0"] + }, { + "name": "measureText", + "args": ["1e+0"] + }, { + "name": "measureText", + "args": ["1e+1"] + }, { + "name": "measureText", + "args": [""] + }, { + "name": "measureText", + "args": [""] + }, { + "name": "measureText", + "args": [""] + }, { + "name": "measureText", + "args": [""] + }, { + "name": "measureText", + "args": ["5e+0"] + }, { + "name": "measureText", + "args": [""] + }, { + "name": "measureText", + "args": [""] + }, { + "name": "measureText", + "args": ["2e+0"] + }, { + "name": "measureText", + "args": ["1e+0"] + }, { + "name": "setFillStyle", + "args": ["#666"] + }, { + "name": "setLineWidth", + "args": [1] + }, { + "name": "setStrokeStyle", + "args": ["rgba(0,0,0,0.25)"] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [28, 0.5] + }, { + "name": "lineTo", + "args": [33, 0.5] + }, { + "name": "moveTo", + "args": [33, 0.5] + }, { + "name": "lineTo", + "args": [133, 0.5] + }, { + "name": "stroke", + "args": [] + }, { + "name": "setLineWidth", + "args": [1] + }, { + "name": "setStrokeStyle", + "args": ["rgba(0, 0, 0, 0.1)"] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [28, 14.089974696520528] + }, { + "name": "lineTo", + "args": [33, 14.089974696520528] + }, { + "name": "moveTo", + "args": [33, 14.089974696520528] + }, { + "name": "lineTo", + "args": [133, 14.089974696520528] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [28, 29.28227386339279] + }, { + "name": "lineTo", + "args": [33, 29.28227386339279] + }, { + "name": "moveTo", + "args": [33, 29.28227386339279] + }, { + "name": "lineTo", + "args": [133, 29.28227386339279] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [28, 46.50588211576573] + }, { + "name": "lineTo", + "args": [33, 46.50588211576573] + }, { + "name": "moveTo", + "args": [33, 46.50588211576573] + }, { + "name": "lineTo", + "args": [133, 46.50588211576573] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [28, 66.38907863605783] + }, { + "name": "lineTo", + "args": [33, 66.38907863605783] + }, { + "name": "moveTo", + "args": [33, 66.38907863605783] + }, { + "name": "lineTo", + "args": [133, 66.38907863605783] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [28, 89.9059087122024] + }, { + "name": "lineTo", + "args": [33, 89.9059087122024] + }, { + "name": "moveTo", + "args": [33, 89.9059087122024] + }, { + "name": "lineTo", + "args": [133, 89.9059087122024] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [28, 118.68818257559516] + }, { + "name": "lineTo", + "args": [33, 118.68818257559516] + }, { + "name": "moveTo", + "args": [33, 118.68818257559516] + }, { + "name": "lineTo", + "args": [133, 118.68818257559516] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [28, 155.79498734826026] + }, { + "name": "lineTo", + "args": [33, 155.79498734826026] + }, { + "name": "moveTo", + "args": [33, 155.79498734826026] + }, { + "name": "lineTo", + "args": [133, 155.79498734826026] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [28, 208.0940912877976] + }, { + "name": "lineTo", + "args": [33, 208.0940912877976] + }, { + "name": "moveTo", + "args": [33, 208.0940912877976] + }, { + "name": "lineTo", + "args": [133, 208.0940912877976] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "moveTo", + "args": [28, 297.5] + }, { + "name": "lineTo", + "args": [33, 297.5] + }, { + "name": "moveTo", + "args": [33, 297.5] + }, { + "name": "lineTo", + "args": [133, 297.5] + }, { + "name": "stroke", + "args": [] + }, { + "name": "fillText", + "args": ["1e+1", 23, 0] + }, { + "name": "fillText", + "args": ["", 23, 13.589974696520528] + }, { + "name": "fillText", + "args": ["", 23, 28.78227386339279] + }, { + "name": "fillText", + "args": ["", 23, 46.00588211576573] + }, { + "name": "fillText", + "args": ["", 23, 65.88907863605783] + }, { + "name": "fillText", + "args": ["5e+0", 23, 89.4059087122024] + }, { + "name": "fillText", + "args": ["", 23, 118.18818257559516] + }, { + "name": "fillText", + "args": ["", 23, 155.29498734826026] + }, { + "name": "fillText", + "args": ["2e+0", 23, 207.5940912877976] + }, { + "name": "fillText", + "args": ["1e+0", 23, 297] + }]; + expect(mockContext.getCalls()).toEqual(expected); + + // Turn off some drawing + config.gridLines.drawTicks = false; + config.gridLines.drawOnChartArea = false; + config.labels.show = false; + + mockContext.resetCalls(); + + verticalScale.draw(); + expect(mockContext.getCalls()).toEqual([{ + "name": "setFillStyle", + "args": ["#666"] + }, { + "name": "setLineWidth", + "args": [1] + }, { + "name": "setStrokeStyle", + "args": ["rgba(0,0,0,0.25)"] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }, { + "name": "setLineWidth", + "args": [1] + }, { + "name": "setStrokeStyle", + "args": ["rgba(0, 0, 0, 0.1)"] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }, { + "name": "beginPath", + "args": [] + }, { + "name": "stroke", + "args": [] + }]); + }); +}); \ No newline at end of file