@@ -79,8 +79,17 @@ var attrs = exports.attributes = {
7979 'for example a sum of dates or average of categories.' ,
8080 '*median* will return the average of the two central values if there is' ,
8181 'an even count. *mode* will return the first value to reach the maximum' ,
82- 'count, in case of a tie. *stddev* uses the population formula' ,
83- '(denominator N, not N-1)'
82+ 'count, in case of a tie.'
83+ ] . join ( ' ' )
84+ } ,
85+ funcmode : {
86+ valType : 'enumerated' ,
87+ values : [ 'sample' , 'population' ] ,
88+ dflt : 'sample' ,
89+ role : 'info' ,
90+ description : [
91+ '*stddev* supports two formula variants: *sample* (normalize by N-1)' ,
92+ 'and *population* (normalize by N).'
8493 ] . join ( ' ' )
8594 } ,
8695 enabled : {
@@ -148,17 +157,24 @@ exports.supplyDefaults = function(transformIn, traceOut) {
148157
149158 var aggregationsIn = transformIn . aggregations ;
150159 var aggregationsOut = transformOut . aggregations = new Array ( aggregationsIn . length ) ;
160+ var aggregationOut ;
161+
162+ function coercei ( attr , dflt ) {
163+ return Lib . coerce ( aggregationsIn [ i ] , aggregationOut , aggAttrs , attr , dflt ) ;
164+ }
151165
152166 if ( aggregationsIn ) {
153167 for ( i = 0 ; i < aggregationsIn . length ; i ++ ) {
154- var aggregationOut = { } ;
155- var target = Lib . coerce ( aggregationsIn [ i ] , aggregationOut , aggAttrs , 'target' ) ;
156- var func = Lib . coerce ( aggregationsIn [ i ] , aggregationOut , aggAttrs , 'func' ) ;
157- var enabledi = Lib . coerce ( aggregationsIn [ i ] , aggregationOut , aggAttrs , 'enabled' ) ;
168+ aggregationOut = { } ;
169+ var target = coercei ( 'target' ) ;
170+ var func = coercei ( 'func' ) ;
171+ var enabledi = coercei ( 'enabled' ) ;
158172
159173 // add this aggregation to the output only if it's the first instance
160174 // of a valid target attribute - or an unused target attribute with "count"
161175 if ( enabledi && target && ( arrayAttrs [ target ] || ( func === 'count' && arrayAttrs [ target ] === undefined ) ) ) {
176+ if ( func === 'stddev' ) coercei ( 'funcmode' ) ;
177+
162178 arrayAttrs [ target ] = 0 ;
163179 aggregationsOut [ i ] = aggregationOut ;
164180 }
@@ -225,7 +241,7 @@ function aggregateOneArray(gd, trace, groupings, aggregation) {
225241 var targetNP = Lib . nestedProperty ( trace , attr ) ;
226242 var arrayIn = targetNP . get ( ) ;
227243 var conversions = Axes . getDataConversions ( gd , trace , attr , arrayIn ) ;
228- var func = getAggregateFunction ( aggregation . func , conversions ) ;
244+ var func = getAggregateFunction ( aggregation , conversions ) ;
229245
230246 var arrayOut = new Array ( groupings . length ) ;
231247 for ( var i = 0 ; i < groupings . length ; i ++ ) {
@@ -234,7 +250,8 @@ function aggregateOneArray(gd, trace, groupings, aggregation) {
234250 targetNP . set ( arrayOut ) ;
235251}
236252
237- function getAggregateFunction ( func , conversions ) {
253+ function getAggregateFunction ( opts , conversions ) {
254+ var func = opts . func ;
238255 var d2c = conversions . d2c ;
239256 var c2d = conversions . c2d ;
240257
@@ -371,7 +388,11 @@ function getAggregateFunction(func, conversions) {
371388 // is a number of milliseconds, and for categories it's a number
372389 // of category differences, which is not generically meaningful but
373390 // as in other cases we don't forbid it.
374- return Math . sqrt ( ( total2 - ( total * total / cnt ) ) / cnt ) ;
391+ var norm = ( opts . funcmode === 'sample' ) ? ( cnt - 1 ) : cnt ;
392+ // this is debatable: should a count of 1 return sample stddev of
393+ // 0 or undefined?
394+ if ( ! norm ) return 0 ;
395+ return Math . sqrt ( ( total2 - ( total * total / cnt ) ) / norm ) ;
375396 } ;
376397 }
377398}
0 commit comments