@@ -79,8 +79,17 @@ var attrs = exports.attributes = {
79
79
'for example a sum of dates or average of categories.' ,
80
80
'*median* will return the average of the two central values if there is' ,
81
81
'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).'
84
93
] . join ( ' ' )
85
94
} ,
86
95
enabled : {
@@ -148,17 +157,24 @@ exports.supplyDefaults = function(transformIn, traceOut) {
148
157
149
158
var aggregationsIn = transformIn . aggregations ;
150
159
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
+ }
151
165
152
166
if ( aggregationsIn ) {
153
167
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' ) ;
158
172
159
173
// add this aggregation to the output only if it's the first instance
160
174
// of a valid target attribute - or an unused target attribute with "count"
161
175
if ( enabledi && target && ( arrayAttrs [ target ] || ( func === 'count' && arrayAttrs [ target ] === undefined ) ) ) {
176
+ if ( func === 'stddev' ) coercei ( 'funcmode' ) ;
177
+
162
178
arrayAttrs [ target ] = 0 ;
163
179
aggregationsOut [ i ] = aggregationOut ;
164
180
}
@@ -225,7 +241,7 @@ function aggregateOneArray(gd, trace, groupings, aggregation) {
225
241
var targetNP = Lib . nestedProperty ( trace , attr ) ;
226
242
var arrayIn = targetNP . get ( ) ;
227
243
var conversions = Axes . getDataConversions ( gd , trace , attr , arrayIn ) ;
228
- var func = getAggregateFunction ( aggregation . func , conversions ) ;
244
+ var func = getAggregateFunction ( aggregation , conversions ) ;
229
245
230
246
var arrayOut = new Array ( groupings . length ) ;
231
247
for ( var i = 0 ; i < groupings . length ; i ++ ) {
@@ -234,7 +250,8 @@ function aggregateOneArray(gd, trace, groupings, aggregation) {
234
250
targetNP . set ( arrayOut ) ;
235
251
}
236
252
237
- function getAggregateFunction ( func , conversions ) {
253
+ function getAggregateFunction ( opts , conversions ) {
254
+ var func = opts . func ;
238
255
var d2c = conversions . d2c ;
239
256
var c2d = conversions . c2d ;
240
257
@@ -371,7 +388,11 @@ function getAggregateFunction(func, conversions) {
371
388
// is a number of milliseconds, and for categories it's a number
372
389
// of category differences, which is not generically meaningful but
373
390
// 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 ) ;
375
396
} ;
376
397
}
377
398
}
0 commit comments