diff --git a/src/transforms/aggregate.js b/src/transforms/aggregate.js index 00d5ac29b37..e3f009c5767 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', 'range'], dflt: 'first', role: 'info', editType: 'calc', @@ -86,7 +86,9 @@ 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 values.', + '*range* will return the difference between the min and max linked values.' ].join(' ') }, funcmode: { @@ -345,6 +347,27 @@ 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 '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': return function(array, indices) { var sortCalc = []; 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); });