From: Ekaterina Dontsova Date: Thu, 11 Aug 2016 19:40:25 +0000 (+0300) Subject: Fix #2966: 0 values in logarithmic scale for line and vertical bar charts (#3016) X-Git-Tag: v2.2.2~10 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=31197978b7052fef053d093258a3f9e40eab8289;p=thirdparty%2FChart.js.git Fix #2966: 0 values in logarithmic scale for line and vertical bar charts (#3016) --- diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js index 9c595b0ac..86cc516af 100644 --- a/src/scales/scale.logarithmic.js +++ b/src/scales/scale.logarithmic.js @@ -12,7 +12,9 @@ module.exports = function(Chart) { callback: function(value, index, arr) { var remain = value / (Math.pow(10, Math.floor(helpers.log10(value)))); - if (remain === 1 || remain === 2 || remain === 5 || index === 0 || index === arr.length - 1) { + if (value === 0){ + return '0'; + } else if (remain === 1 || remain === 2 || remain === 5 || index === 0 || index === arr.length - 1) { return value.toExponential(); } else { return ''; @@ -38,6 +40,7 @@ module.exports = function(Chart) { // Calculate Range me.min = null; me.max = null; + me.minNotZero = null; if (opts.stacked) { var valuesPerType = {}; @@ -96,6 +99,10 @@ module.exports = function(Chart) { } else if (value > me.max) { me.max = value; } + + if(value !== 0 && (me.minNotZero === null || value < me.minNotZero)) { + me.minNotZero = value; + } }); } }); @@ -134,8 +141,16 @@ module.exports = function(Chart) { while (tickVal < me.max) { ticks.push(tickVal); - var exp = Math.floor(helpers.log10(tickVal)); - var significand = Math.floor(tickVal / Math.pow(10, exp)) + 1; + var exp; + var significand; + + if(tickVal === 0){ + exp = Math.floor(helpers.log10(me.minNotZero)); + significand = Math.round(me.minNotZero / Math.pow(10, exp)); + } else { + exp = Math.floor(helpers.log10(tickVal)); + significand = Math.floor(tickVal / Math.pow(10, exp)) + 1; + } if (significand === 10) { significand = 1; @@ -187,13 +202,15 @@ module.exports = function(Chart) { var start = me.start; var newVal = +me.getRightValue(value); - var range = helpers.log10(me.end) - helpers.log10(start); + var range; var paddingTop = me.paddingTop; var paddingBottom = me.paddingBottom; var paddingLeft = me.paddingLeft; + var opts = me.options; + var tickOpts = opts.ticks; if (me.isHorizontal()) { - + range = helpers.log10(me.end) - helpers.log10(start); // todo: if start === 0 if (newVal === 0) { pixel = me.left + paddingLeft; } else { @@ -203,14 +220,31 @@ module.exports = function(Chart) { } } else { // Bottom - top since pixels increase downard on a screen - if (newVal === 0) { - pixel = me.top + paddingTop; + innerDimension = me.height - (paddingTop + paddingBottom); + if(start === 0 && !tickOpts.reverse){ + range = helpers.log10(me.end) - helpers.log10(me.minNotZero); + if (newVal === start) { + pixel = me.bottom - paddingBottom; + } else if(newVal === me.minNotZero){ + pixel = me.bottom - paddingBottom - innerDimension * 0.02; + } else { + pixel = me.bottom - paddingBottom - innerDimension * 0.02 - (innerDimension * 0.98/ range * (helpers.log10(newVal)-helpers.log10(me.minNotZero))); + } + } else if (me.end === 0 && tickOpts.reverse){ + range = helpers.log10(me.start) - helpers.log10(me.minNotZero); + if (newVal === me.end) { + pixel = me.top + paddingTop; + } else if(newVal === me.minNotZero){ + pixel = me.top + paddingTop + innerDimension * 0.02; + } else { + pixel = me.top + paddingTop + innerDimension * 0.02 + (innerDimension * 0.98/ range * (helpers.log10(newVal)-helpers.log10(me.minNotZero))); + } } else { + range = helpers.log10(me.end) - helpers.log10(start); innerDimension = me.height - (paddingTop + paddingBottom); pixel = (me.bottom - paddingBottom) - (innerDimension / range * (helpers.log10(newVal) - helpers.log10(start))); - } + } } - return pixel; }, getValueForPixel: function(pixel) { @@ -221,11 +255,10 @@ module.exports = function(Chart) { if (me.isHorizontal()) { innerDimension = me.width - (me.paddingLeft + me.paddingRight); value = me.start * Math.pow(10, (pixel - me.left - me.paddingLeft) * range / innerDimension); - } else { + } else { // todo: if start === 0 innerDimension = me.height - (me.paddingTop + me.paddingBottom); value = Math.pow(10, (me.bottom - me.paddingBottom - pixel) * range / innerDimension) / me.start; } - return value; } }); diff --git a/test/scale.logarithmic.tests.js b/test/scale.logarithmic.tests.js index dc75480bb..c167c4edf 100644 --- a/test/scale.logarithmic.tests.js +++ b/test/scale.logarithmic.tests.js @@ -67,6 +67,12 @@ describe('Logarithmic Scale tests', function() { }, { yAxisID: 'yScale1', data: [150] + }, { + yAxisID: 'yScale2', + data: [20, 0, 150, 1800, 3040] + }, { + yAxisID: 'yScale3', + data: [67, 0.0004, 0, 820, 0.001] }], labels: ['a', 'b', 'c', 'd', 'e'] }, @@ -78,6 +84,14 @@ describe('Logarithmic Scale tests', function() { }, { id: 'yScale1', type: 'logarithmic' + }, + { + id: 'yScale2', + type: 'logarithmic' + }, + { + id: 'yScale3', + type: 'logarithmic' }] } } @@ -90,6 +104,14 @@ describe('Logarithmic Scale tests', function() { expect(chart.scales.yScale1).not.toEqual(undefined); // must construct expect(chart.scales.yScale1.min).toBe(1); expect(chart.scales.yScale1.max).toBe(5000); + + expect(chart.scales.yScale2).not.toEqual(undefined); // must construct + expect(chart.scales.yScale2.min).toBe(0); + expect(chart.scales.yScale2.max).toBe(4000); + + expect(chart.scales.yScale3).not.toEqual(undefined); // must construct + expect(chart.scales.yScale3.min).toBe(0); + expect(chart.scales.yScale3.max).toBe(900); }); it('should correctly determine the max & min of string data values', function() { @@ -105,6 +127,12 @@ describe('Logarithmic Scale tests', function() { }, { yAxisID: 'yScale1', data: ['150'] + }, { + yAxisID: 'yScale2', + data: ['20', '0', '150', '1800', '3040'] + }, { + yAxisID: 'yScale3', + data: ['67', '0.0004', '0', '820', '0.001'] }], labels: ['a', 'b', 'c', 'd', 'e'] }, @@ -116,6 +144,13 @@ describe('Logarithmic Scale tests', function() { }, { id: 'yScale1', type: 'logarithmic' + }, { + id: 'yScale2', + type: 'logarithmic' + }, + { + id: 'yScale3', + type: 'logarithmic' }] } } @@ -128,6 +163,14 @@ describe('Logarithmic Scale tests', function() { expect(chart.scales.yScale1).not.toEqual(undefined); // must construct expect(chart.scales.yScale1.min).toBe(1); expect(chart.scales.yScale1.max).toBe(5000); + + expect(chart.scales.yScale2).not.toEqual(undefined); // must construct + expect(chart.scales.yScale2.min).toBe(0); + expect(chart.scales.yScale2.max).toBe(4000); + + expect(chart.scales.yScale3).not.toEqual(undefined); // must construct + expect(chart.scales.yScale3.min).toBe(0); + expect(chart.scales.yScale3.max).toBe(900); }); it('should correctly determine the max & min data values when there are hidden datasets', function() { @@ -144,6 +187,13 @@ describe('Logarithmic Scale tests', function() { yAxisID: 'yScale1', data: [50000], hidden: true + }, { + yAxisID: 'yScale2', + data: [20, 0, 7400, 14, 291] + }, { + yAxisID: 'yScale2', + data: [6, 0.0007, 9, 890, 60000], + hidden: true }], labels: ['a', 'b', 'c', 'd', 'e'] }, @@ -155,6 +205,9 @@ describe('Logarithmic Scale tests', function() { }, { id: 'yScale1', type: 'logarithmic' + }, { + id: 'yScale2', + type: 'logarithmic' }] } } @@ -163,6 +216,10 @@ describe('Logarithmic Scale tests', function() { expect(chart.scales.yScale1).not.toEqual(undefined); // must construct expect(chart.scales.yScale1.min).toBe(1); expect(chart.scales.yScale1.max).toBe(5000); + + expect(chart.scales.yScale2).not.toEqual(undefined); // must construct + expect(chart.scales.yScale2.min).toBe(0); + expect(chart.scales.yScale2.max).toBe(8000); }); it('should correctly determine the max & min data values when there is NaN data', function() { @@ -170,32 +227,47 @@ describe('Logarithmic Scale tests', function() { type: 'bar', data: { datasets: [{ + yAxisID: 'yScale0', data: [undefined, 10, null, 5, 5000, NaN, 78, 450] }, { + yAxisID: 'yScale0', data: [undefined, 28, null, 1000, 500, NaN, 50, 42] + }, { + yAxisID: 'yScale1', + data: [undefined, 30, null, 9400, 0, NaN, 54, 836] + }, { + yAxisID: 'yScale1', + data: [undefined, 0, null, 800, 9, NaN, 894, 21] }], labels: ['a', 'b', 'c', 'd', 'e', 'f' ,'g'] }, options: { scales: { yAxes: [{ - id: 'yScale', + id: 'yScale0', + type: 'logarithmic' + }, { + id: 'yScale1', type: 'logarithmic' }] } } }); - expect(chart.scales.yScale).not.toEqual(undefined); // must construct - expect(chart.scales.yScale.min).toBe(1); - expect(chart.scales.yScale.max).toBe(5000); + expect(chart.scales.yScale0).not.toEqual(undefined); // must construct + expect(chart.scales.yScale0.min).toBe(1); + expect(chart.scales.yScale0.max).toBe(5000); // Turn on stacked mode since it uses it's own chart.options.scales.yAxes[0].stacked = true; chart.update(); - expect(chart.scales.yScale.min).toBe(10); - expect(chart.scales.yScale.max).toBe(6000); + expect(chart.scales.yScale0.min).toBe(10); + expect(chart.scales.yScale0.max).toBe(6000); + + expect(chart.scales.yScale1).not.toEqual(undefined); // must construct + expect(chart.scales.yScale1.min).toBe(0); + expect(chart.scales.yScale1.max).toBe(10000); }); it('should correctly determine the max & min for scatter data', function() { @@ -233,6 +305,41 @@ describe('Logarithmic Scale tests', function() { expect(chart.scales.yScale.max).toBe(200); }); + it('should correctly determine the max & min for scatter data when 0 values are present', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [ + { x: 7, y: 950 }, + { x: 289, y: 0 }, + { x: 0, y: 8 }, + { x: 23, y: 0.04 } + ] + }] + }, + options: { + scales: { + xAxes: [{ + id: 'xScale', + type: 'logarithmic', + position: 'bottom' + }], + yAxes: [{ + id: 'yScale', + type: 'logarithmic' + }] + } + } + }); + + expect(chart.scales.xScale.min).toBe(0); + expect(chart.scales.xScale.max).toBe(300); + + expect(chart.scales.yScale.min).toBe(0); + expect(chart.scales.yScale.max).toBe(1000); + }); + it('should correctly determine the min and max data values when stacked mode is turned on', function() { var chart = window.acquireChart({ type: 'bar', @@ -410,6 +517,39 @@ describe('Logarithmic Scale tests', function() { })); }); + it('should generate tick marks when 0 values are present', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [11, 0.8, 0, 28, 7] + }], + labels: [] + }, + options: { + scales: { + yAxes: [{ + id: 'yScale', + type: 'logarithmic', + ticks: { + callback: function(value) { + return value; + } + } + }] + } + } + }); + + // Counts down because the lines are drawn top to bottom + expect(chart.scales.yScale).toEqual(jasmine.objectContaining({ + ticks: [30, 20, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0.9, 0.8, 0], + start: 0, + end: 30 + })); + }); + + it('should generate tick marks in the correct order in reversed mode', function() { var chart = window.acquireChart({ type: 'line', @@ -443,12 +583,45 @@ describe('Logarithmic Scale tests', function() { })); }); + it('should generate tick marks in the correct order in reversed mode when 0 values are present', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [21, 9, 0, 10, 25] + }], + labels: [] + }, + options: { + scales: { + yAxes: [{ + id: 'yScale', + type: 'logarithmic', + ticks: { + reverse: true, + callback: function(value) { + return value; + } + } + }] + } + } + }); + + // Counts down because the lines are drawn top to bottom + expect(chart.scales.yScale).toEqual(jasmine.objectContaining({ + ticks: [0, 9, 10, 20, 30], + start: 30, + end: 0 + })); + }); + it('should build labels using the default template', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ - data: [10, 5, 1, 25, 78] + data: [10, 5, 1, 25, 0, 78] }], labels: [] }, @@ -462,7 +635,7 @@ describe('Logarithmic Scale tests', function() { } }); - expect(chart.scales.yScale.ticks).toEqual(['8e+1', '', '', '5e+1', '', '', '2e+1', '1e+1', '', '', '', '', '5e+0', '', '', '2e+0', '1e+0']); + expect(chart.scales.yScale.ticks).toEqual(['8e+1', '', '', '5e+1', '', '', '2e+1', '1e+1', '', '', '', '', '5e+0', '', '', '2e+0', '1e+0', '0']); }); it('should build labels using the user supplied callback', function() { @@ -570,4 +743,48 @@ describe('Logarithmic Scale tests', function() { expect(yScale.getValueForPixel(456)).toBeCloseTo(1, 1e-4); expect(yScale.getValueForPixel(234)).toBeCloseTo(10, 1e-4); }); + + it('should get the correct pixel value for a point when 0 values are present', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'yScale', + data: [0.063, 4, 0, 63, 10, 0.5] + }], + labels: [] + }, + options: { + scales: { + yAxes: [{ + id: 'yScale', + type: 'logarithmic', + ticks: { + reverse: false + } + }] + } + } + }); + + var yScale = chart.scales.yScale; + expect(yScale.getPixelForValue(70, 0, 0)).toBeCloseToPixel(32); // top + paddingTop + expect(yScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(484); // bottom - paddingBottom + expect(yScale.getPixelForValue(0.063, 0, 0)).toBeCloseToPixel(475); // minNotZero 2% from range + expect(yScale.getPixelForValue(0.5, 0, 0)).toBeCloseToPixel(344); + expect(yScale.getPixelForValue(4, 0, 0)).toBeCloseToPixel(213); + expect(yScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(155); + expect(yScale.getPixelForValue(63, 0, 0)).toBeCloseToPixel(38.5); + + chart.options.scales.yAxes[0].ticks.reverse = true; // Reverse mode + chart.update(); + + expect(yScale.getPixelForValue(70, 0, 0)).toBeCloseToPixel(484); // bottom - paddingBottom + expect(yScale.getPixelForValue(0, 0, 0)).toBeCloseToPixel(32); // top + paddingTop + expect(yScale.getPixelForValue(0.063, 0, 0)).toBeCloseToPixel(41); // minNotZero 2% from range + expect(yScale.getPixelForValue(0.5, 0, 0)).toBeCloseToPixel(172); + expect(yScale.getPixelForValue(4, 0, 0)).toBeCloseToPixel(303); + expect(yScale.getPixelForValue(10, 0, 0)).toBeCloseToPixel(361); + expect(yScale.getPixelForValue(63, 0, 0)).toBeCloseToPixel(477); + }); });