From b706c333b82e7f60bbd484f06d693673daf25e55 Mon Sep 17 00:00:00 2001 From: "Hogan,Brendan P" Date: Thu, 28 Jun 2018 11:40:34 -0400 Subject: [PATCH 1/4] add aggregate change function --- src/transforms/aggregate.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/transforms/aggregate.js b/src/transforms/aggregate.js index 00d5ac29b37..33771a41b62 100644 --- a/src/transforms/aggregate.js +++ b/src/transforms/aggregate.js @@ -68,7 +68,7 @@ var attrs = exports.attributes = { }, func: { valType: 'enumerated', - values: ['count', 'sum', 'avg', 'median', 'mode', 'rms', 'stddev', 'min', 'max', 'first', 'last'], + values: ['count', 'sum', 'avg', 'median', 'mode', 'rms', 'stddev', 'min', 'max', 'first', 'last', 'change'], dflt: 'first', role: 'info', editType: 'calc', @@ -86,7 +86,8 @@ var attrs = exports.attributes = { 'for example a sum of dates or average of categories.', '*median* will return the average of the two central values if there is', 'an even count. *mode* will return the first value to reach the maximum', - 'count, in case of a tie.' + 'count, in case of a tie.', + '*change* will return the difference between the first and last linked value.' ].join(' ') }, funcmode: { @@ -297,6 +298,8 @@ function getAggregateFunction(opts, conversions) { return first; case 'last': return last; + case 'change': + return change; case 'sum': // This will produce output in all cases even though it's nonsensical @@ -441,3 +444,7 @@ function first(array, indices) { function last(array, indices) { return array[indices[indices.length - 1]]; } + +function change(array, indices) { + return last(array, indices) - first(array, indices); +} From c172bf74c715302c0ecb0bf16a316c4075782634 Mon Sep 17 00:00:00 2001 From: "Hogan,Brendan P" Date: Thu, 28 Jun 2018 11:53:07 -0400 Subject: [PATCH 2/4] add aggregate range function --- src/transforms/aggregate.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/transforms/aggregate.js b/src/transforms/aggregate.js index 33771a41b62..c28df5c51a1 100644 --- a/src/transforms/aggregate.js +++ b/src/transforms/aggregate.js @@ -68,7 +68,7 @@ var attrs = exports.attributes = { }, func: { valType: 'enumerated', - values: ['count', 'sum', 'avg', 'median', 'mode', 'rms', 'stddev', 'min', 'max', 'first', 'last', 'change'], + values: ['count', 'sum', 'avg', 'median', 'mode', 'rms', 'stddev', 'min', 'max', 'first', 'last', 'change', 'range'], dflt: 'first', role: 'info', editType: 'calc', @@ -87,7 +87,8 @@ var attrs = exports.attributes = { '*median* will return the average of the two central values if there is', 'an even count. *mode* will return the first value to reach the maximum', 'count, in case of a tie.', - '*change* will return the difference between the first and last linked value.' + '*change* will return the difference between the first and last linked values.', + '*range* will return the difference between the min and max linked values.' ].join(' ') }, funcmode: { @@ -348,6 +349,20 @@ function getAggregateFunction(opts, conversions) { return (out === -Infinity) ? BADNUM : c2d(out); }; + case 'range': + return function(array, indices) { + var min = Infinity; + var max = -Infinity; + for(var i = 0; i < indices.length; i++) { + var vi = d2c(array[indices[i]]); + if(vi !== BADNUM) { + min = Math.min(min, vi); + max = Math.max(max, vi); + }; + } + return (max === -Infinity || min === Infinity) ? BADNUM : c2d(max - min); + }; + case 'median': return function(array, indices) { var sortCalc = []; From 0f99934a2ebddabe4cbca237ccd5c3a43e4ce195 Mon Sep 17 00:00:00 2001 From: "Hogan,Brendan P" Date: Thu, 28 Jun 2018 15:28:16 -0400 Subject: [PATCH 3/4] protect against non-numerics in change function --- src/transforms/aggregate.js | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/transforms/aggregate.js b/src/transforms/aggregate.js index c28df5c51a1..e3f009c5767 100644 --- a/src/transforms/aggregate.js +++ b/src/transforms/aggregate.js @@ -299,8 +299,6 @@ function getAggregateFunction(opts, conversions) { return first; case 'last': return last; - case 'change': - return change; case 'sum': // This will produce output in all cases even though it's nonsensical @@ -350,17 +348,24 @@ function getAggregateFunction(opts, conversions) { }; case 'range': - return function(array, indices) { - var min = Infinity; - var max = -Infinity; - for(var i = 0; i < indices.length; i++) { - var vi = d2c(array[indices[i]]); - if(vi !== BADNUM) { - min = Math.min(min, vi); - max = Math.max(max, vi); - }; + return function(array, indices) { + var min = Infinity; + var max = -Infinity; + for(var i = 0; i < indices.length; i++) { + var vi = d2c(array[indices[i]]); + if(vi !== BADNUM) { + min = Math.min(min, vi); + max = Math.max(max, vi); } - return (max === -Infinity || min === Infinity) ? BADNUM : c2d(max - min); + } + return (max === -Infinity || min === Infinity) ? BADNUM : c2d(max - min); + }; + + case 'change': + return function(array, indices) { + var first = d2c(array[indices[0]]); + var last = d2c(array[indices[indices.length - 1]]); + return (first === BADNUM || last === BADNUM) ? BADNUM : c2d(last - first); }; case 'median': @@ -459,7 +464,3 @@ function first(array, indices) { function last(array, indices) { return array[indices[indices.length - 1]]; } - -function change(array, indices) { - return last(array, indices) - first(array, indices); -} From 165b5bf4a31a586cc82291f2d7497411398b9de4 Mon Sep 17 00:00:00 2001 From: "Hogan,Brendan P" Date: Thu, 28 Jun 2018 15:59:19 -0400 Subject: [PATCH 4/4] add tests for aggregate functions change and range --- test/jasmine/tests/transform_aggregate_test.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/jasmine/tests/transform_aggregate_test.js b/test/jasmine/tests/transform_aggregate_test.js index 9eb5e227afc..2161e1475b7 100644 --- a/test/jasmine/tests/transform_aggregate_test.js +++ b/test/jasmine/tests/transform_aggregate_test.js @@ -15,6 +15,7 @@ describe('aggregate', function() { Plotly.newPlot(gd, [{ x: [1, 2, 3, 4, 'fail'], y: [1.1, 2.2, 3.3, 'nope', 5.5], + customdata: [4, 'nope', 3, 2, 1], marker: { size: ['2001-01-01', 0.2, 0.1, 0.4, 0.5], color: [2, 4, '', 10, 8], @@ -34,6 +35,7 @@ describe('aggregate', function() { {target: 'x', func: 'sum'}, // non-numerics will not count toward numerator or denominator for avg {target: 'y', func: 'avg'}, + {target: 'customdata', func: 'change'}, {target: 'marker.size', func: 'min'}, {target: 'marker.color', func: 'max'}, // marker.opacity doesn't have an entry, but it will default to first @@ -54,6 +56,7 @@ describe('aggregate', function() { expect(traceOut.x).toEqual([8, 2]); expect(traceOut.y).toBeCloseToArray([3.3, 2.2], 5); + expect(traceOut.customdata).toEqual([-3, undefined]); expect(traceOut.marker.size).toEqual([0.1, 0.2]); expect(traceOut.marker.color).toEqual([10, 4]); expect(traceOut.marker.opacity).toEqual([0.6, 'boo']); @@ -221,15 +224,17 @@ describe('aggregate', function() { expect(inverseMapping).toEqual({0: [0, 1, 4], 1: [2, 3]}); }); - it('handles median, mode, rms, & stddev for numeric data', function() { + it('handles median, mode, rms, stddev, change & range for numeric data', function() { // again, nothing is going to barf with non-numeric data, but sometimes it // won't make much sense. Plotly.newPlot(gd, [{ x: [1, 1, 2, 2, 1], y: [1, 2, 3, 4, 5], + customdata: [5, 4, 3, 2, 1], marker: { size: [1, 2, 3, 4, 5], + opacity: [0.6, 0.5, 0.2, 0.8, 1.0], line: {width: [1, 1, 2, 2, 1]}, color: [1, 1, 2, 2, 1] }, @@ -239,7 +244,9 @@ describe('aggregate', function() { aggregations: [ {target: 'x', func: 'mode'}, {target: 'y', func: 'median'}, + {target: 'customdata', func: 'change'}, {target: 'marker.size', func: 'rms'}, + {target: 'marker.opacity', func: 'range'}, {target: 'marker.line.width', func: 'stddev', funcmode: 'population'}, {target: 'marker.color', func: 'stddev'} ] @@ -252,7 +259,9 @@ describe('aggregate', function() { // but 2 gets to that count first expect(traceOut.x).toEqual([2, 1]); expect(traceOut.y).toBeCloseToArray([3.5, 2], 5); + expect(traceOut.customdata).toEqual([-4, 0]); expect(traceOut.marker.size).toBeCloseToArray([Math.sqrt(51 / 4), 2], 5); + expect(traceOut.marker.opacity).toEqual([0.8, 0]); expect(traceOut.marker.line.width).toBeCloseToArray([0.5, 0], 5); expect(traceOut.marker.color).toBeCloseToArray([Math.sqrt(1 / 3), 0], 5); });