diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index aa8b06a2cbc..483ac1f43ba 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -32,7 +32,7 @@ defaults._set('bar', { * @private */ function computeMinSampleSize(scale, pixels) { - var min = scale.isHorizontal() ? scale.width : scale.height; + var min = scale._length; var ticks = scale.getTicks(); var prev, curr, i, ilen; @@ -42,7 +42,7 @@ function computeMinSampleSize(scale, pixels) { for (i = 0, ilen = ticks.length; i < ilen; ++i) { curr = scale.getPixelForTick(i); - min = i > 0 ? Math.min(min, curr - prev) : min; + min = i > 0 ? Math.min(min, Math.abs(curr - prev)) : min; prev = curr; } @@ -262,9 +262,6 @@ module.exports = DatasetController.extend({ var scale = me._getIndexScale(); var stackCount = me.getStackCount(); var datasetIndex = me.index; - var isHorizontal = scale.isHorizontal(); - var start = isHorizontal ? scale.left : scale.top; - var end = start + (isHorizontal ? scale.width : scale.height); var pixels = []; var i, ilen, min; @@ -279,8 +276,8 @@ module.exports = DatasetController.extend({ return { min: min, pixels: pixels, - start: start, - end: end, + start: scale._start, + end: scale._end, stackCount: stackCount, scale: scale }; diff --git a/src/controllers/controller.horizontalBar.js b/src/controllers/controller.horizontalBar.js index ebfc6d84ae1..30e6846c121 100644 --- a/src/controllers/controller.horizontalBar.js +++ b/src/controllers/controller.horizontalBar.js @@ -23,6 +23,9 @@ defaults._set('horizontalBar', { offset: true, gridLines: { offsetGridLines: true + }, + ticks: { + reverse: true } }] }, diff --git a/src/core/core.layouts.js b/src/core/core.layouts.js index c91bf6e701c..c19d3698951 100644 --- a/src/core/core.layouts.js +++ b/src/core/core.layouts.js @@ -186,6 +186,10 @@ function placeBoxes(boxes, chartArea, params) { box.height = box.bottom - box.top; x = box.right; } + + if (box._configure) { + box._configure(); + } } chartArea.x = x; diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 298d409aa8e..5461aa9173c 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -70,9 +70,7 @@ function getPixelForGridLine(scale, index, offsetGridLines) { if (offsetGridLines) { if (scale.getTicks().length === 1) { - lineValue -= scale.isHorizontal() ? - Math.max(lineValue - scale.left, scale.right - lineValue) : - Math.max(lineValue - scale.top, scale.bottom - lineValue); + lineValue -= Math.max(lineValue - scale._start, scale._end - lineValue); } else if (index === 0) { lineValue -= (scale.getPixelForTick(1) - lineValue) / 2; } else { @@ -267,6 +265,8 @@ var Scale = Element.extend({ me.setDimensions(); me.afterSetDimensions(); + me._configure(); + // Data min/max me.beforeDataLimits(); me.determineDataLimits(); @@ -331,6 +331,25 @@ var Scale = Element.extend({ return me.minSize; }, + + /** + * @private + */ + _configure: function() { + var me = this; + + if (me.isHorizontal()) { + me._start = me.left; + me._end = me.right; + me._reverse = me.options.ticks.reverse; + } else { + me._start = me.top; + me._end = me.bottom; + me._reverse = !me.options.ticks.reverse; + } + me._length = me._end - me._start; + }, + afterUpdate: function() { helpers.callback(this.options.afterUpdate, [this]); }, @@ -580,7 +599,7 @@ var Scale = Element.extend({ return this.options.position === 'top' || this.options.position === 'bottom'; }, isFullWidth: function() { - return (this.options.fullWidth); + return this.options.fullWidth; }, // Get the correct value. NaN bad inputs, If the value type is object get the x or y based on whether we are horizontal or not @@ -679,20 +698,11 @@ var Scale = Element.extend({ var me = this; var offset = me.options.offset; var numTicks = me._ticks.length; - if (index < 0 || index > numTicks - 1) { - return null; - } - if (me.isHorizontal()) { - var tickWidth = me.width / Math.max((numTicks - (offset ? 0 : 1)), 1); - var pixel = (tickWidth * index); + var tickWidth = 1 / Math.max((numTicks - (offset ? 0 : 1)), 1); - if (offset) { - pixel += tickWidth / 2; - } - - return me.left + pixel; - } - return me.top + (index * (me.height / (numTicks - 1))); + return index < 0 || index > numTicks - 1 + ? null + : me.getPixelForDecimal(index * tickWidth + (offset ? tickWidth / 2 : 0)); }, /** @@ -701,9 +711,19 @@ var Scale = Element.extend({ */ getPixelForDecimal: function(decimal) { var me = this; - return me.isHorizontal() - ? me.left + decimal * me.width - : me.top + decimal * me.height; + + if (me._reverse) { + decimal = 1 - decimal; + } + + return me._start + decimal * me._length; + }, + + getDecimalForPixel: function(pixel) { + var me = this; + var decimal = (pixel - me._start) / me._length; + + return Math.min(1, Math.max(0, me._reverse ? 1 - decimal : decimal)); }, /** @@ -731,7 +751,6 @@ var Scale = Element.extend({ */ _autoSkip: function(ticks) { var me = this; - var isHorizontal = me.isHorizontal(); var optionTicks = me.options.ticks; var tickCount = ticks.length; var skipRatio = false; @@ -741,9 +760,7 @@ var Scale = Element.extend({ // drawn as their center at end of axis, so tickCount-1 var ticksLength = me._tickSize() * (tickCount - 1); - // Axis length - var axisLength = isHorizontal ? me.width : me.height; - + var axisLength = me._length; var result = []; var i, tick; @@ -774,7 +791,6 @@ var Scale = Element.extend({ */ _tickSize: function() { var me = this; - var isHorizontal = me.isHorizontal(); var optionTicks = me.options.ticks; // Calculate space needed by label in axis direction. @@ -788,7 +804,7 @@ var Scale = Element.extend({ var h = labelSizes ? labelSizes.highest.height + padding : 0; // Calculate space needed for 1 tick in axis direction. - return isHorizontal + return me.isHorizontal() ? h * cos > w * sin ? w / cos : h / sin : h * sin < w * cos ? h / cos : w / sin; }, @@ -1116,7 +1132,7 @@ var Scale = Element.extend({ var scaleLabelX, scaleLabelY; if (me.isHorizontal()) { - scaleLabelX = me.left + ((me.right - me.left) / 2); // midpoint of the width + scaleLabelX = me.left + me.width / 2; // midpoint of the width scaleLabelY = position === 'bottom' ? me.bottom - halfLineHeight - scaleLabelPadding.bottom : me.top + halfLineHeight + scaleLabelPadding.top; @@ -1125,7 +1141,7 @@ var Scale = Element.extend({ scaleLabelX = isLeft ? me.left + halfLineHeight + scaleLabelPadding.top : me.right - halfLineHeight - scaleLabelPadding.top; - scaleLabelY = me.top + ((me.bottom - me.top) / 2); + scaleLabelY = me.top + me.height / 2; rotation = isLeft ? -0.5 * Math.PI : 0.5 * Math.PI; } diff --git a/src/scales/scale.category.js b/src/scales/scale.category.js index 4cb9422230d..9cef04ff1bb 100644 --- a/src/scales/scale.category.js +++ b/src/scales/scale.category.js @@ -63,17 +63,21 @@ module.exports = Scale.extend({ return me.ticks[index - me.minIndex]; }, - // Used to get data value locations. Value can either be an index or a numerical value - getPixelForValue: function(value, index, datasetIndex) { + _getParams: function() { var me = this; var offset = me.options.offset; - // 1 is added because we need the length but we have the indexes - var offsetAmt = Math.max(me.maxIndex + 1 - me.minIndex - (offset ? 0 : 1), 1); + return { + start: me.minIndex - (offset ? 0.5 : 0), + range: Math.max(me.ticks.length - (offset ? 0 : 1), 1) + }; + }, - var isHorizontal = me.isHorizontal(); - var valueDimension = (isHorizontal ? me.width : me.height) / offsetAmt; - var valueCategory, labels, idx, pixel; + // Used to get data value locations. Value can either be an index or a numerical value + getPixelForValue: function(value, index, datasetIndex) { + var me = this; + var params = me._getParams(); + var valueCategory, labels, idx; if (!isNullOrUndef(index) && !isNullOrUndef(datasetIndex)) { value = me.chart.data.datasets[datasetIndex].data[index]; @@ -82,22 +86,18 @@ module.exports = Scale.extend({ // If value is a data object, then index is the index in the data array, // not the index of the scale. We need to change that. if (!isNullOrUndef(value)) { - valueCategory = isHorizontal ? value.x : value.y; + valueCategory = me.isHorizontal() ? value.x : value.y; } if (valueCategory !== undefined || (value !== undefined && isNaN(index))) { labels = me._getLabels(); value = helpers.valueOrDefault(valueCategory, value); idx = labels.indexOf(value); index = idx !== -1 ? idx : index; + if (isNaN(index)) { + index = value; + } } - - pixel = valueDimension * (index - me.minIndex); - - if (offset) { - pixel += valueDimension / 2; - } - - return (isHorizontal ? me.left : me.top) + pixel; + return me.getPixelForDecimal((index - params.start) / params.range); }, getPixelForTick: function(index) { @@ -110,25 +110,9 @@ module.exports = Scale.extend({ getValueForPixel: function(pixel) { var me = this; - var offset = me.options.offset; - var offsetAmt = Math.max(me._ticks.length - (offset ? 0 : 1), 1); - var isHorizontal = me.isHorizontal(); - var valueDimension = (isHorizontal ? me.width : me.height) / offsetAmt; - var value; - - pixel -= isHorizontal ? me.left : me.top; - - if (offset) { - pixel -= valueDimension / 2; - } - - if (pixel <= 0) { - value = 0; - } else { - value = Math.round(pixel / valueDimension); - } - - return value + me.minIndex; + var params = me._getParams(); + var value = Math.round(params.start + me.getDecimalForPixel(pixel) * params.range); + return Math.min(Math.max(value, 0), me.ticks.length - 1); }, getBasePixel: function() { diff --git a/src/scales/scale.linear.js b/src/scales/scale.linear.js index a76f4593c11..398fce8fc7a 100644 --- a/src/scales/scale.linear.js +++ b/src/scales/scale.linear.js @@ -161,26 +161,17 @@ module.exports = LinearScaleBase.extend({ // This must be called after fit has been run so that // this.left, this.top, this.right, and this.bottom have been defined var me = this; - var start = me.start; - + var start = me.min; var rightValue = +me.getRightValue(value); - var pixel; - var range = me.end - start; - - if (me.isHorizontal()) { - pixel = me.left + (me.width / range * (rightValue - start)); - } else { - pixel = me.bottom - (me.height / range * (rightValue - start)); - } - return pixel; + var range = me.max - start; + return me.getPixelForDecimal((rightValue - start) / range); }, getValueForPixel: function(pixel) { var me = this; - var isHorizontal = me.isHorizontal(); - var innerDimension = isHorizontal ? me.width : me.height; - var offset = (isHorizontal ? pixel - me.left : me.bottom - pixel) / innerDimension; - return me.start + ((me.end - me.start) * offset); + var start = me.min; + var range = me.max - start; + return start + me.getDecimalForPixel(pixel) * range; }, getPixelForTick: function(index) { diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 1229e8d40a3..9ec6e4316c4 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -6,6 +6,7 @@ var Scale = require('../core/core.scale'); var Ticks = require('../core/core.ticks'); var valueOrDefault = helpers.valueOrDefault; +var log10 = helpers.math.log10; /** * Generate a set of logarithmic ticks @@ -16,20 +17,20 @@ var valueOrDefault = helpers.valueOrDefault; function generateTicks(generationOptions, dataRange) { var ticks = []; - var tickVal = valueOrDefault(generationOptions.min, Math.pow(10, Math.floor(helpers.log10(dataRange.min)))); + var tickVal = valueOrDefault(generationOptions.min, Math.pow(10, Math.floor(log10(dataRange.min)))); - var endExp = Math.floor(helpers.log10(dataRange.max)); + var endExp = Math.floor(log10(dataRange.max)); var endSignificand = Math.ceil(dataRange.max / Math.pow(10, endExp)); var exp, significand; if (tickVal === 0) { - exp = Math.floor(helpers.log10(dataRange.minNotZero)); + exp = Math.floor(log10(dataRange.minNotZero)); significand = Math.floor(dataRange.minNotZero / Math.pow(10, exp)); ticks.push(tickVal); tickVal = significand * Math.pow(10, exp); } else { - exp = Math.floor(helpers.log10(tickVal)); + exp = Math.floor(log10(tickVal)); significand = Math.floor(tickVal / Math.pow(10, exp)); } var precision = exp < 0 ? Math.pow(10, Math.abs(exp)) : 1; @@ -180,26 +181,26 @@ module.exports = Scale.extend({ if (me.min === me.max) { if (me.min !== 0 && me.min !== null) { - me.min = Math.pow(10, Math.floor(helpers.log10(me.min)) - 1); - me.max = Math.pow(10, Math.floor(helpers.log10(me.max)) + 1); + me.min = Math.pow(10, Math.floor(log10(me.min)) - 1); + me.max = Math.pow(10, Math.floor(log10(me.max)) + 1); } else { me.min = DEFAULT_MIN; me.max = DEFAULT_MAX; } } if (me.min === null) { - me.min = Math.pow(10, Math.floor(helpers.log10(me.max)) - 1); + me.min = Math.pow(10, Math.floor(log10(me.max)) - 1); } if (me.max === null) { me.max = me.min !== 0 - ? Math.pow(10, Math.floor(helpers.log10(me.min)) + 1) + ? Math.pow(10, Math.floor(log10(me.min)) + 1) : DEFAULT_MAX; } if (me.minNotZero === null) { if (me.min > 0) { me.minNotZero = me.min; } else if (me.max < 1) { - me.minNotZero = Math.pow(10, Math.floor(helpers.log10(me.max))); + me.minNotZero = Math.pow(10, Math.floor(log10(me.max))); } else { me.minNotZero = DEFAULT_MIN; } @@ -261,87 +262,48 @@ module.exports = Scale.extend({ * @private */ _getFirstTickValue: function(value) { - var exp = Math.floor(helpers.log10(value)); + var exp = Math.floor(log10(value)); var significand = Math.floor(value / Math.pow(10, exp)); return significand * Math.pow(10, exp); }, - getPixelForValue: function(value) { + _getParams: function() { var me = this; - var tickOpts = me.options.ticks; - var reverse = tickOpts.reverse; - var log10 = helpers.log10; - var firstTickValue = me._getFirstTickValue(me.minNotZero); + var start = me.min; var offset = 0; - var innerDimension, pixel, start, end, sign; + if (start === 0) { + start = me._getFirstTickValue(me.minNotZero); + offset = valueOrDefault(me.options.ticks.fontSize, defaults.global.defaultFontSize) / me._length; + } + return { + start: log10(start), + offset: offset, + range: (log10(me.max) - log10(start)) / (1 - offset) + }; + }, + + getPixelForValue: function(value) { + var me = this; + var decimal = 0; + var params; value = +me.getRightValue(value); - if (reverse) { - start = me.end; - end = me.start; - sign = -1; - } else { - start = me.start; - end = me.end; - sign = 1; - } - if (me.isHorizontal()) { - innerDimension = me.width; - pixel = reverse ? me.right : me.left; - } else { - innerDimension = me.height; - sign *= -1; // invert, since the upper-left corner of the canvas is at pixel (0, 0) - pixel = reverse ? me.top : me.bottom; - } - if (value !== start) { - if (start === 0) { // include zero tick - offset = valueOrDefault(tickOpts.fontSize, defaults.global.defaultFontSize); - innerDimension -= offset; - start = firstTickValue; - } - if (value !== 0) { - offset += innerDimension / (log10(end) - log10(start)) * (log10(value) - log10(start)); - } - pixel += sign * offset; + + if (value > me.min && value > 0) { + params = me._getParams(); + decimal = (log10(value) - params.start) / params.range + params.offset; } - return pixel; + return me.getPixelForDecimal(decimal); }, getValueForPixel: function(pixel) { var me = this; - var tickOpts = me.options.ticks; - var reverse = tickOpts.reverse; - var log10 = helpers.log10; - var firstTickValue = me._getFirstTickValue(me.minNotZero); - var innerDimension, start, end, value; - - if (reverse) { - start = me.end; - end = me.start; - } else { - start = me.start; - end = me.end; - } - if (me.isHorizontal()) { - innerDimension = me.width; - value = reverse ? me.right - pixel : pixel - me.left; - } else { - innerDimension = me.height; - value = reverse ? pixel - me.top : me.bottom - pixel; - } - if (value !== start) { - if (start === 0) { // include zero tick - var offset = valueOrDefault(tickOpts.fontSize, defaults.global.defaultFontSize); - value -= offset; - innerDimension -= offset; - start = firstTickValue; - } - value *= log10(end) - log10(start); - value /= innerDimension; - value = Math.pow(10, log10(start) + value); - } - return value; + var params = me._getParams(); + var decimal = me.getDecimalForPixel(pixel); + return decimal === 0 && me.min === 0 + ? 0 + : Math.pow(10, params.start + (decimal - params.offset) * params.range); } }); diff --git a/test/specs/core.scale.tests.js b/test/specs/core.scale.tests.js index 605707eb3c1..880b4b56ce6 100644 --- a/test/specs/core.scale.tests.js +++ b/test/specs/core.scale.tests.js @@ -94,7 +94,7 @@ describe('Core.scale', function() { labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'], offsetGridLines: true, offset: true, - expected: [-0.5, 102.5, 204.5, 307.5, 409.5] + expected: [0.5, 102.5, 204.5, 307.5, 409.5] }, { labels: ['tick1'], offsetGridLines: false, @@ -185,6 +185,7 @@ describe('Core.scale', function() { drawTicks: false }, ticks: { + reverse: true, display: false }, offset: test.offset diff --git a/test/specs/scale.category.tests.js b/test/specs/scale.category.tests.js index 96883e31c9c..1a55dbb6b06 100644 --- a/test/specs/scale.category.tests.js +++ b/test/specs/scale.category.tests.js @@ -324,7 +324,10 @@ describe('Category scale tests', function() { yAxes: [{ id: 'yScale0', type: 'category', - position: 'left' + position: 'left', + ticks: { + reverse: true + } }] } } @@ -371,6 +374,7 @@ describe('Category scale tests', function() { type: 'category', position: 'left', ticks: { + reverse: true, min: '2', max: '4' } @@ -465,7 +469,7 @@ describe('Category scale tests', function() { var yScale = chart.scales.yScale0; expect(yScale.getPixelForValue({x: 0, y: 2}, 0, 0)).toBeCloseToPixel(257); - expect(yScale.getPixelForValue({x: 0, y: 1}, 4, 0)).toBeCloseToPixel(144); + expect(yScale.getPixelForValue({x: 0, y: 1}, 4, 0)).toBeCloseToPixel(370); }); it('Should get the correct pixel for an object value in a bar chart', function() { diff --git a/test/specs/scale.linear.tests.js b/test/specs/scale.linear.tests.js index 383d7480d44..64d9620c72d 100644 --- a/test/specs/scale.linear.tests.js +++ b/test/specs/scale.linear.tests.js @@ -1195,4 +1195,86 @@ describe('Linear Scale', function() { expect(chart.scales.yScale0).not.toEqual(undefined); // must construct expect(chart.scales.yScale0.max).toBeGreaterThan(chart.scales.yScale0.min); }); + + it('Should get correct pixel values when horizontal', function() { + var chart = window.acquireChart({ + type: 'horizontalBar', + data: { + datasets: [{ + data: [0.05, -25, 10, 15, 20, 25, 30, 35] + }] + }, + options: { + scales: { + xAxes: [{ + id: 'x', + type: 'linear', + }] + } + } + }); + + var start = chart.chartArea.left; + var end = chart.chartArea.right; + var min = -30; + var max = 40; + var scale = chart.scales.x; + + expect(scale.getPixelForValue(max)).toBeCloseToPixel(end); + expect(scale.getPixelForValue(min)).toBeCloseToPixel(start); + expect(scale.getValueForPixel(end)).toBeCloseTo(max, 4); + expect(scale.getValueForPixel(start)).toBeCloseTo(min, 4); + + scale.options.ticks.reverse = true; + chart.update(); + + start = chart.chartArea.left; + end = chart.chartArea.right; + + expect(scale.getPixelForValue(max)).toBeCloseToPixel(start); + expect(scale.getPixelForValue(min)).toBeCloseToPixel(end); + expect(scale.getValueForPixel(end)).toBeCloseTo(min, 4); + expect(scale.getValueForPixel(start)).toBeCloseTo(max, 4); + }); + + it('Should get correct pixel values when vertical', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [0.05, -25, 10, 15, 20, 25, 30, 35] + }] + }, + options: { + scales: { + yAxes: [{ + id: 'y', + type: 'linear', + }] + } + } + }); + + var start = chart.chartArea.bottom; + var end = chart.chartArea.top; + var min = -30; + var max = 40; + var scale = chart.scales.y; + + expect(scale.getPixelForValue(max)).toBeCloseToPixel(end); + expect(scale.getPixelForValue(min)).toBeCloseToPixel(start); + expect(scale.getValueForPixel(end)).toBeCloseTo(max, 4); + expect(scale.getValueForPixel(start)).toBeCloseTo(min, 4); + + scale.options.ticks.reverse = true; + chart.update(); + + start = chart.chartArea.bottom; + end = chart.chartArea.top; + + expect(scale.getPixelForValue(max)).toBeCloseToPixel(start); + expect(scale.getPixelForValue(min)).toBeCloseToPixel(end); + expect(scale.getValueForPixel(end)).toBeCloseTo(min, 4); + expect(scale.getValueForPixel(start)).toBeCloseTo(max, 4); + }); }); diff --git a/test/specs/scale.logarithmic.tests.js b/test/specs/scale.logarithmic.tests.js index dd7c7cce94a..c1c048c7c01 100644 --- a/test/specs/scale.logarithmic.tests.js +++ b/test/specs/scale.logarithmic.tests.js @@ -862,6 +862,8 @@ describe('Logarithmic Scale tests', function() { type: 'logarithmic' }]; Chart.helpers.extend(scaleConfig, setup.scale); + scaleConfig[setup.axis + 'Axes'][0].type = 'logarithmic'; + var description = 'dataset has stack option and ' + setup.describe + ' and axis is "' + setup.axis + '";'; describe(description, function() {