From e90e91d339bd7fb0510fac09c71bac51beb0cfd0 Mon Sep 17 00:00:00 2001 From: Jukka Kurkela Date: Thu, 13 Jun 2019 22:23:47 +0300 Subject: [PATCH] Refactor scales --- src/controllers/controller.bar.js | 11 +- src/controllers/controller.horizontalBar.js | 3 + src/core/core.layouts.js | 4 + src/core/core.scale.js | 67 +++++++----- src/scales/scale.category.js | 74 +++++-------- src/scales/scale.linear.js | 21 ++-- src/scales/scale.logarithmic.js | 114 +++++++------------- test/specs/core.scale.tests.js | 3 +- test/specs/scale.category.tests.js | 6 +- test/specs/scale.linear.tests.js | 82 ++++++++++++++ test/specs/scale.logarithmic.tests.js | 2 + 11 files changed, 213 insertions(+), 174 deletions(-) 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 4a0969f62ba..e3613e0cb1b 100644 --- a/src/core/core.layouts.js +++ b/src/core/core.layouts.js @@ -376,6 +376,10 @@ module.exports = { // Move to next point left = box.right; } + + if (box._configure) { + box._configure(); + } } helpers.each(leftBoxes.concat(topBoxes), placeBox); diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 7d14be21c24..b0bdc3cfdd3 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -267,6 +267,8 @@ var Scale = Element.extend({ me.setDimensions(); me.afterSetDimensions(); + me._configure(); + // Data min/max me.beforeDataLimits(); me.determineDataLimits(); @@ -331,6 +333,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]); }, @@ -581,7 +602,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,21 +700,9 @@ var Scale = Element.extend({ getPixelForTick: function(index) { var me = this; var offset = me.options.offset; - if (me.isHorizontal()) { - var innerWidth = me.width - (me.paddingLeft + me.paddingRight); - var tickWidth = innerWidth / Math.max((me._ticks.length - (offset ? 0 : 1)), 1); - var pixel = (tickWidth * index) + me.paddingLeft; - - if (offset) { - pixel += tickWidth / 2; - } - - var finalVal = me.left + pixel; - finalVal += me.isFullWidth() ? me.margins.left : 0; - return finalVal; - } - var innerHeight = me.height - (me.paddingTop + me.paddingBottom); - return me.top + (index * (innerHeight / (me._ticks.length - 1))); + var tickWidth = 1 / Math.max((me._ticks.length - (offset ? 0 : 1)), 1); + var decimal = index * tickWidth + (offset ? tickWidth / 2 : 0); + return me.getPixelForDecimal(decimal); }, /** @@ -702,15 +711,19 @@ var Scale = Element.extend({ */ getPixelForDecimal: function(decimal) { var me = this; - if (me.isHorizontal()) { - var innerWidth = me.width - (me.paddingLeft + me.paddingRight); - var valueOffset = (innerWidth * decimal) + me.paddingLeft; - var finalVal = me.left + valueOffset; - finalVal += me.isFullWidth() ? me.margins.left : 0; - return finalVal; + if (me._reverse) { + decimal = 1 - decimal; } - return me.top + (decimal * me.height); + + 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)); }, /** @@ -738,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; @@ -749,9 +761,7 @@ var Scale = Element.extend({ var ticksLength = me._tickSize() * (tickCount - 1); // Axis length - var axisLength = isHorizontal - ? me.width - (me.paddingLeft + me.paddingRight) - : me.height - (me.paddingTop + me.PaddingBottom); + var axisLength = me._length; var result = []; var i, tick; @@ -783,7 +793,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. @@ -797,7 +806,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; }, diff --git a/src/scales/scale.category.js b/src/scales/scale.category.js index 1d8949d4df6..d01c23decf7 100644 --- a/src/scales/scale.category.js +++ b/src/scales/scale.category.js @@ -1,6 +1,7 @@ 'use strict'; var Scale = require('../core/core.scale'); +var helpers = require('../helpers/index'); var defaultConfig = { position: 'bottom' @@ -48,71 +49,54 @@ 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) { + _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); + var range = Math.max((me.maxIndex + 1 - me.minIndex - (offset ? 0 : 1)), 1); + var start = me.minIndex - (offset ? 0.5 : 0); + return { + start: start, + range: range + }; + }, + + // Used to get data value locations. Value can either be an index or a numerical value + getPixelForValue: function(value, index) { + var me = this; + var params = me._getParams(); + var valueCategory, labels, idx; // 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. - var valueCategory; - if (value !== undefined && value !== null) { + if (!helpers.isNullOrUndef(value)) { valueCategory = me.isHorizontal() ? value.x : value.y; } if (valueCategory !== undefined || (value !== undefined && isNaN(index))) { - var labels = me._getLabels(); + labels = me._getLabels(); value = valueCategory || value; - var idx = labels.indexOf(value); + idx = labels.indexOf(value); index = idx !== -1 ? idx : index; - } - - if (me.isHorizontal()) { - var valueWidth = me.width / offsetAmt; - var widthOffset = (valueWidth * (index - me.minIndex)); - - if (offset) { - widthOffset += (valueWidth / 2); + if (isNaN(index)) { + index = value; } - - return me.left + widthOffset; } - var valueHeight = me.height / offsetAmt; - var heightOffset = (valueHeight * (index - me.minIndex)); - - if (offset) { - heightOffset += (valueHeight / 2); - } - - return me.top + heightOffset; + return me.getPixelForDecimal((index - params.start) / params.range); }, getPixelForTick: function(index) { - return this.getPixelForValue(this.ticks[index], index + this.minIndex, null); + var me = this; + var ticks = me.ticks; + if (index < 0 || index > ticks.length - 1) { + return null; + } + return me.getPixelForValue(ticks[index], index + me.minIndex); }, getValueForPixel: function(pixel) { var me = this; - var offset = me.options.offset; - var value; - var offsetAmt = Math.max((me._ticks.length - (offset ? 0 : 1)), 1); - var horz = me.isHorizontal(); - var valueDimension = (horz ? me.width : me.height) / offsetAmt; - - pixel -= horz ? 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(); + return Math.round(params.start + me.getDecimalForPixel(pixel) * params.range); }, getBasePixel: function() { diff --git a/src/scales/scale.linear.js b/src/scales/scale.linear.js index c9ce8709ded..e4ce49428bd 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 3e9b2849f0b..fc992cc290d 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; } @@ -257,87 +258,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 d6529324ec3..184c0f68791 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' } diff --git a/test/specs/scale.linear.tests.js b/test/specs/scale.linear.tests.js index bdf8a9386f9..d8d463c2b05 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() {