10
10
11
11
var isNumeric = require ( 'fast-isnumeric' ) ;
12
12
13
+ var Axes = require ( '../../plots/cartesian/axes' ) ;
13
14
var Lib = require ( '../../lib' ) ;
15
+
16
+ var BADNUM = require ( '../../constants/numerical' ) . BADNUM ;
14
17
var _ = Lib . _ ;
15
- var Axes = require ( '../../plots/cartesian/axes' ) ;
16
18
17
- // outlier definition based on http://www.physics.csbsju.edu/stats/box2.html
18
19
module . exports = function calc ( gd , trace ) {
19
20
var fullLayout = gd . _fullLayout ;
20
21
var xa = Axes . getFromId ( gd , trace . xaxis || 'x' ) ;
@@ -24,7 +25,7 @@ module.exports = function calc(gd, trace) {
24
25
// N.B. violin reuses same Box.calc
25
26
var numKey = trace . type === 'violin' ? '_numViolins' : '_numBoxes' ;
26
27
27
- var i ;
28
+ var i , j ;
28
29
var valAxis , valLetter ;
29
30
var posAxis , posLetter ;
30
31
@@ -40,127 +41,230 @@ module.exports = function calc(gd, trace) {
40
41
posLetter = 'x' ;
41
42
}
42
43
43
- var val = valAxis . makeCalcdata ( trace , valLetter ) ;
44
- var pos = getPos ( trace , posLetter , posAxis , val , fullLayout [ numKey ] ) ;
45
-
46
- var dv = Lib . distinctVals ( pos ) ;
44
+ var posArray = getPos ( trace , posLetter , posAxis , fullLayout [ numKey ] ) ;
45
+ var dv = Lib . distinctVals ( posArray ) ;
47
46
var posDistinct = dv . vals ;
48
47
var dPos = dv . minDiff / 2 ;
49
- var posBins = makeBins ( posDistinct , dPos ) ;
50
-
51
- var pLen = posDistinct . length ;
52
- var ptsPerBin = initNestedArray ( pLen ) ;
53
-
54
- // bin pts info per position bins
55
- for ( i = 0 ; i < trace . _length ; i ++ ) {
56
- var v = val [ i ] ;
57
- if ( ! isNumeric ( v ) ) continue ;
58
-
59
- var n = Lib . findBin ( pos [ i ] , posBins ) ;
60
- if ( n >= 0 && n < pLen ) {
61
- var pt = { v : v , i : i } ;
62
- arraysToCalcdata ( pt , trace , i ) ;
63
- ptsPerBin [ n ] . push ( pt ) ;
64
- }
65
- }
66
48
49
+ // item in trace calcdata
67
50
var cdi ;
51
+ // array of {v: v, i, i} sample pts
52
+ var pts ;
53
+ // values of the `pts` array of objects
54
+ var boxVals ;
55
+ // length of sample
56
+ var N ;
57
+ // single sample point
58
+ var pt ;
59
+ // single sample value
60
+ var v ;
61
+
62
+ // filter function for outlier pts
63
+ // outlier definition based on http://www.physics.csbsju.edu/stats/box2.html
68
64
var ptFilterFn = ( trace . boxpoints || trace . points ) === 'all' ?
69
65
Lib . identity :
70
66
function ( pt ) { return ( pt . v < cdi . lf || pt . v > cdi . uf ) ; } ;
71
67
72
- var minLowerNotch = Infinity ;
73
- var maxUpperNotch = - Infinity ;
68
+ if ( trace . _hasPreCompStats ) {
69
+ var valArrayRaw = trace [ valLetter ] ;
70
+ var d2c = function ( k ) { return valAxis . d2c ( ( trace [ k ] || [ ] ) [ i ] ) ; } ;
71
+ var minVal = Infinity ;
72
+ var maxVal = - Infinity ;
74
73
75
- // build calcdata trace items, one item per distinct position
76
- for ( i = 0 ; i < pLen ; i ++ ) {
77
- if ( ptsPerBin [ i ] . length > 0 ) {
78
- var pts = ptsPerBin [ i ] . sort ( sortByVal ) ;
79
- var boxVals = pts . map ( extractVal ) ;
80
- var N = boxVals . length ;
74
+ for ( i = 0 ; i < trace . _length ; i ++ ) {
75
+ var posi = posArray [ i ] ;
76
+ if ( ! isNumeric ( posi ) ) continue ;
81
77
82
78
cdi = { } ;
83
- cdi . pos = posDistinct [ i ] ;
84
- cdi . pts = pts ;
85
-
86
- // Sort categories by values
87
- cdi [ posLetter ] = cdi . pos ;
88
- cdi [ valLetter ] = cdi . pts . map ( extractVal ) ;
89
-
90
- cdi . min = boxVals [ 0 ] ;
91
- cdi . max = boxVals [ N - 1 ] ;
92
- cdi . mean = Lib . mean ( boxVals , N ) ;
93
- cdi . sd = Lib . stdev ( boxVals , N , cdi . mean ) ;
94
-
95
- // median
96
- cdi . med = Lib . interp ( boxVals , 0.5 ) ;
97
-
98
- var quartilemethod = trace . quartilemethod ;
99
-
100
- if ( ( N % 2 ) && ( quartilemethod === 'exclusive' || quartilemethod === 'inclusive' ) ) {
101
- var lower ;
102
- var upper ;
103
-
104
- if ( quartilemethod === 'exclusive' ) {
105
- // do NOT include the median in either half
106
- lower = boxVals . slice ( 0 , N / 2 ) ;
107
- upper = boxVals . slice ( N / 2 + 1 ) ;
108
- } else if ( quartilemethod === 'inclusive' ) {
109
- // include the median in either half
110
- lower = boxVals . slice ( 0 , N / 2 + 1 ) ;
111
- upper = boxVals . slice ( N / 2 ) ;
79
+ cdi . pos = cdi [ posLetter ] = posi ;
80
+
81
+ cdi . q1 = d2c ( 'q1' ) ;
82
+ cdi . med = d2c ( 'median' ) ;
83
+ cdi . q3 = d2c ( 'q3' ) ;
84
+
85
+ pts = [ ] ;
86
+ if ( valArrayRaw && Lib . isArrayOrTypedArray ( valArrayRaw [ i ] ) ) {
87
+ for ( j = 0 ; j < valArrayRaw [ i ] . length ; j ++ ) {
88
+ v = valAxis . d2c ( valArrayRaw [ i ] [ j ] ) ;
89
+ if ( v !== BADNUM ) {
90
+ pt = { v : v , i : [ i , j ] } ;
91
+ arraysToCalcdata ( pt , trace , [ i , j ] ) ;
92
+ pts . push ( pt ) ;
93
+ }
112
94
}
113
-
114
- cdi . q1 = Lib . interp ( lower , 0.5 ) ;
115
- cdi . q3 = Lib . interp ( upper , 0.5 ) ;
95
+ }
96
+ cdi . pts = pts . sort ( sortByVal ) ;
97
+ boxVals = cdi [ valLetter ] = pts . map ( extractVal ) ;
98
+ N = boxVals . length ;
99
+
100
+ if ( cdi . med !== BADNUM && cdi . q1 !== BADNUM && cdi . q3 !== BADNUM &&
101
+ cdi . med >= cdi . q1 && cdi . q3 >= cdi . med
102
+ ) {
103
+ var lf = d2c ( 'lowerfence' ) ;
104
+ cdi . lf = ( lf !== BADNUM && lf <= cdi . q1 ) ?
105
+ lf :
106
+ computeLowerFence ( cdi , boxVals , N ) ;
107
+
108
+ var uf = d2c ( 'upperfence' ) ;
109
+ cdi . uf = ( uf !== BADNUM && uf >= cdi . q3 ) ?
110
+ uf :
111
+ computeUpperFence ( cdi , boxVals , N ) ;
112
+
113
+ var mean = d2c ( 'mean' ) ;
114
+ cdi . mean = ( mean !== BADNUM ) ?
115
+ mean :
116
+ ( N ? Lib . mean ( boxVals , N ) : ( cdi . q1 + cdi . q3 ) / 2 ) ;
117
+
118
+ var sd = d2c ( 'sd' ) ;
119
+ cdi . sd = ( mean !== BADNUM && sd >= 0 ) ?
120
+ sd :
121
+ ( N ? Lib . stdev ( boxVals , N , cdi . mean ) : ( cdi . q3 - cdi . q1 ) ) ;
122
+
123
+ cdi . lo = computeLowerOutlierBound ( cdi ) ;
124
+ cdi . uo = computeUpperOutlierBound ( cdi ) ;
125
+
126
+ var ns = d2c ( 'notchspan' ) ;
127
+ ns = ( ns !== BADNUM && ns > 0 ) ? ns : computeNotchSpan ( cdi , N ) ;
128
+ cdi . ln = cdi . med - ns ;
129
+ cdi . un = cdi . med + ns ;
130
+
131
+ var imin = cdi . lf ;
132
+ var imax = cdi . uf ;
133
+ if ( trace . boxpoints && boxVals . length ) {
134
+ imin = Math . min ( imin , boxVals [ 0 ] ) ;
135
+ imax = Math . max ( imax , boxVals [ N - 1 ] ) ;
136
+ }
137
+ if ( trace . notched ) {
138
+ imin = Math . min ( imin , cdi . ln ) ;
139
+ imax = Math . max ( imax , cdi . un ) ;
140
+ }
141
+ cdi . min = imin ;
142
+ cdi . max = imax ;
116
143
} else {
117
- cdi . q1 = Lib . interp ( boxVals , 0.25 ) ;
118
- cdi . q3 = Lib . interp ( boxVals , 0.75 ) ;
144
+ Lib . warn ( 'Invalid input - make sure that q1 <= median <= q3' ) ;
145
+
146
+ var v0 ;
147
+ if ( cdi . med !== BADNUM ) {
148
+ v0 = cdi . med ;
149
+ } else if ( cdi . q1 !== BADNUM ) {
150
+ if ( cdi . q3 !== BADNUM ) v0 = ( cdi . q1 + cdi . q3 ) / 2 ;
151
+ else v0 = cdi . q1 ;
152
+ } else if ( cdi . q3 !== BADNUM ) {
153
+ v0 = cdi . q3 ;
154
+ } else {
155
+ v0 = 0 ;
156
+ }
157
+
158
+ // draw box as line segment
159
+ cdi . med = v0 ;
160
+ cdi . q1 = cdi . q3 = v0 ;
161
+ cdi . lf = cdi . uf = v0 ;
162
+ cdi . mean = cdi . sd = v0 ;
163
+ cdi . ln = cdi . un = v0 ;
164
+ cdi . min = cdi . max = v0 ;
119
165
}
120
166
121
- // lower and upper fences - last point inside
122
- // 1.5 interquartile ranges from quartiles
123
- cdi . lf = Math . min (
124
- cdi . q1 ,
125
- boxVals [ Math . min (
126
- Lib . findBin ( 2.5 * cdi . q1 - 1.5 * cdi . q3 , boxVals , true ) + 1 ,
127
- N - 1
128
- ) ]
129
- ) ;
130
- cdi . uf = Math . max (
131
- cdi . q3 ,
132
- boxVals [ Math . max (
133
- Lib . findBin ( 2.5 * cdi . q3 - 1.5 * cdi . q1 , boxVals ) ,
134
- 0
135
- ) ]
136
- ) ;
137
-
138
- // lower and upper outliers - 3 IQR out (don't clip to max/min,
139
- // this is only for discriminating suspected & far outliers)
140
- cdi . lo = 4 * cdi . q1 - 3 * cdi . q3 ;
141
- cdi . uo = 4 * cdi . q3 - 3 * cdi . q1 ;
142
-
143
- // lower and upper notches ~95% Confidence Intervals for median
144
- var iqr = cdi . q3 - cdi . q1 ;
145
- var mci = 1.57 * iqr / Math . sqrt ( N ) ;
146
- cdi . ln = cdi . med - mci ;
147
- cdi . un = cdi . med + mci ;
148
- minLowerNotch = Math . min ( minLowerNotch , cdi . ln ) ;
149
- maxUpperNotch = Math . max ( maxUpperNotch , cdi . un ) ;
167
+ minVal = Math . min ( minVal , cdi . min ) ;
168
+ maxVal = Math . max ( maxVal , cdi . max ) ;
150
169
151
170
cdi . pts2 = pts . filter ( ptFilterFn ) ;
152
171
153
172
cd . push ( cdi ) ;
154
173
}
174
+
175
+ trace . _extremes [ valAxis . _id ] = Axes . findExtremes ( valAxis ,
176
+ [ minVal , maxVal ] ,
177
+ { padded : true }
178
+ ) ;
179
+ } else {
180
+ var valArray = valAxis . makeCalcdata ( trace , valLetter ) ;
181
+ var quartilemethod = trace . quartilemethod ;
182
+ var posBins = makeBins ( posDistinct , dPos ) ;
183
+ var pLen = posDistinct . length ;
184
+ var ptsPerBin = initNestedArray ( pLen ) ;
185
+
186
+ // bin pts info per position bins
187
+ for ( i = 0 ; i < trace . _length ; i ++ ) {
188
+ v = valArray [ i ] ;
189
+ if ( ! isNumeric ( v ) ) continue ;
190
+
191
+ var n = Lib . findBin ( posArray [ i ] , posBins ) ;
192
+ if ( n >= 0 && n < pLen ) {
193
+ pt = { v : v , i : i } ;
194
+ arraysToCalcdata ( pt , trace , i ) ;
195
+ ptsPerBin [ n ] . push ( pt ) ;
196
+ }
197
+ }
198
+
199
+ var minLowerNotch = Infinity ;
200
+ var maxUpperNotch = - Infinity ;
201
+
202
+ // build calcdata trace items, one item per distinct position
203
+ for ( i = 0 ; i < pLen ; i ++ ) {
204
+ if ( ptsPerBin [ i ] . length > 0 ) {
205
+ cdi = { } ;
206
+ cdi . pos = cdi [ posLetter ] = posDistinct [ i ] ;
207
+
208
+ pts = cdi . pts = ptsPerBin [ i ] . sort ( sortByVal ) ;
209
+ boxVals = cdi [ valLetter ] = pts . map ( extractVal ) ;
210
+ N = boxVals . length ;
211
+
212
+ cdi . min = boxVals [ 0 ] ;
213
+ cdi . max = boxVals [ N - 1 ] ;
214
+ cdi . mean = Lib . mean ( boxVals , N ) ;
215
+ cdi . sd = Lib . stdev ( boxVals , N , cdi . mean ) ;
216
+ cdi . med = Lib . interp ( boxVals , 0.5 ) ;
217
+
218
+ if ( ( N % 2 ) && ( quartilemethod === 'exclusive' || quartilemethod === 'inclusive' ) ) {
219
+ var lower ;
220
+ var upper ;
221
+
222
+ if ( quartilemethod === 'exclusive' ) {
223
+ // do NOT include the median in either half
224
+ lower = boxVals . slice ( 0 , N / 2 ) ;
225
+ upper = boxVals . slice ( N / 2 + 1 ) ;
226
+ } else if ( quartilemethod === 'inclusive' ) {
227
+ // include the median in either half
228
+ lower = boxVals . slice ( 0 , N / 2 + 1 ) ;
229
+ upper = boxVals . slice ( N / 2 ) ;
230
+ }
231
+
232
+ cdi . q1 = Lib . interp ( lower , 0.5 ) ;
233
+ cdi . q3 = Lib . interp ( upper , 0.5 ) ;
234
+ } else {
235
+ cdi . q1 = Lib . interp ( boxVals , 0.25 ) ;
236
+ cdi . q3 = Lib . interp ( boxVals , 0.75 ) ;
237
+ }
238
+
239
+ // lower and upper fences
240
+ cdi . lf = computeLowerFence ( cdi , boxVals , N ) ;
241
+ cdi . uf = computeUpperFence ( cdi , boxVals , N ) ;
242
+
243
+ // lower and upper outliers bounds
244
+ cdi . lo = computeLowerOutlierBound ( cdi ) ;
245
+ cdi . uo = computeUpperOutlierBound ( cdi ) ;
246
+
247
+ // lower and upper notches
248
+ var mci = computeNotchSpan ( cdi , N ) ;
249
+ cdi . ln = cdi . med - mci ;
250
+ cdi . un = cdi . med + mci ;
251
+ minLowerNotch = Math . min ( minLowerNotch , cdi . ln ) ;
252
+ maxUpperNotch = Math . max ( maxUpperNotch , cdi . un ) ;
253
+
254
+ cdi . pts2 = pts . filter ( ptFilterFn ) ;
255
+
256
+ cd . push ( cdi ) ;
257
+ }
258
+ }
259
+
260
+ trace . _extremes [ valAxis . _id ] = Axes . findExtremes ( valAxis ,
261
+ trace . notched ? valArray . concat ( [ minLowerNotch , maxUpperNotch ] ) : valArray ,
262
+ { padded : true }
263
+ ) ;
155
264
}
156
265
157
266
calcSelection ( cd , trace ) ;
158
267
159
- trace . _extremes [ valAxis . _id ] = Axes . findExtremes ( valAxis ,
160
- trace . notched ? val . concat ( [ minLowerNotch , maxUpperNotch ] ) : val ,
161
- { padded : true }
162
- ) ;
163
-
164
268
if ( cd . length > 0 ) {
165
269
cd [ 0 ] . t = {
166
270
num : fullLayout [ numKey ] ,
@@ -191,14 +295,17 @@ module.exports = function calc(gd, trace) {
191
295
// so if you want one box
192
296
// per trace, set x0 (y0) to the x (y) value or category for this trace
193
297
// (or set x (y) to a constant array matching y (x))
194
- function getPos ( trace , posLetter , posAxis , val , num ) {
195
- if ( posLetter in trace ) {
298
+ function getPos ( trace , posLetter , posAxis , num ) {
299
+ var hasPosArray = posLetter in trace ;
300
+ var hasPos0 = posLetter + '0' in trace ;
301
+ var hasPosStep = 'd' + posLetter in trace ;
302
+
303
+ if ( hasPosArray || ( hasPos0 && hasPosStep ) ) {
196
304
return posAxis . makeCalcdata ( trace , posLetter ) ;
197
305
}
198
306
199
307
var pos0 ;
200
-
201
- if ( posLetter + '0' in trace ) {
308
+ if ( hasPos0 ) {
202
309
pos0 = trace [ posLetter + '0' ] ;
203
310
} else if ( 'name' in trace && (
204
311
posAxis . type === 'category' || (
@@ -218,7 +325,11 @@ function getPos(trace, posLetter, posAxis, val, num) {
218
325
posAxis . r2c_just_indices ( pos0 ) :
219
326
posAxis . d2c ( pos0 , 0 , trace [ posLetter + 'calendar' ] ) ;
220
327
221
- return val . map ( function ( ) { return pos0c ; } ) ;
328
+ var len = trace . _length ;
329
+ var out = new Array ( len ) ;
330
+ for ( var i = 0 ; i < len ; i ++ ) out [ i ] = pos0c ;
331
+
332
+ return out ;
222
333
}
223
334
224
335
function makeBins ( x , dx ) {
@@ -241,15 +352,21 @@ function initNestedArray(len) {
241
352
return arr ;
242
353
}
243
354
244
- function arraysToCalcdata ( pt , trace , i ) {
245
- var trace2calc = {
246
- text : 'tx' ,
247
- hovertext : 'htx'
248
- } ;
355
+ var TRACE_TO_CALC = {
356
+ text : 'tx' ,
357
+ hovertext : 'htx'
358
+ } ;
249
359
250
- for ( var k in trace2calc ) {
251
- if ( Array . isArray ( trace [ k ] ) ) {
252
- pt [ trace2calc [ k ] ] = trace [ k ] [ i ] ;
360
+ function arraysToCalcdata ( pt , trace , ptNumber ) {
361
+ for ( var k in TRACE_TO_CALC ) {
362
+ if ( Lib . isArrayOrTypedArray ( trace [ k ] ) ) {
363
+ if ( Array . isArray ( ptNumber ) ) {
364
+ if ( Lib . isArrayOrTypedArray ( trace [ k ] [ ptNumber [ 0 ] ] ) ) {
365
+ pt [ TRACE_TO_CALC [ k ] ] = trace [ k ] [ ptNumber [ 0 ] ] [ ptNumber [ 1 ] ] ;
366
+ }
367
+ } else {
368
+ pt [ TRACE_TO_CALC [ k ] ] = trace [ k ] [ ptNumber ] ;
369
+ }
253
370
}
254
371
}
255
372
}
@@ -272,3 +389,45 @@ function calcSelection(cd, trace) {
272
389
function sortByVal ( a , b ) { return a . v - b . v ; }
273
390
274
391
function extractVal ( o ) { return o . v ; }
392
+
393
+ // last point below 1.5 * IQR
394
+ function computeLowerFence ( cdi , boxVals , N ) {
395
+ if ( N === 0 ) return cdi . q1 ;
396
+ return Math . min (
397
+ cdi . q1 ,
398
+ boxVals [ Math . min (
399
+ Lib . findBin ( 2.5 * cdi . q1 - 1.5 * cdi . q3 , boxVals , true ) + 1 ,
400
+ N - 1
401
+ ) ]
402
+ ) ;
403
+ }
404
+
405
+ // last point above 1.5 * IQR
406
+ function computeUpperFence ( cdi , boxVals , N ) {
407
+ if ( N === 0 ) return cdi . q3 ;
408
+ return Math . max (
409
+ cdi . q3 ,
410
+ boxVals [ Math . max (
411
+ Lib . findBin ( 2.5 * cdi . q3 - 1.5 * cdi . q1 , boxVals ) ,
412
+ 0
413
+ ) ]
414
+ ) ;
415
+ }
416
+
417
+ // 3 IQR below (don't clip to max/min,
418
+ // this is only for discriminating suspected & far outliers)
419
+ function computeLowerOutlierBound ( cdi ) {
420
+ return 4 * cdi . q1 - 3 * cdi . q3 ;
421
+ }
422
+
423
+ // 3 IQR above (don't clip to max/min,
424
+ // this is only for discriminating suspected & far outliers)
425
+ function computeUpperOutlierBound ( cdi ) {
426
+ return 4 * cdi . q3 - 3 * cdi . q1 ;
427
+ }
428
+
429
+ // 95% confidence intervals for median
430
+ function computeNotchSpan ( cdi , N ) {
431
+ if ( N === 0 ) return 0 ;
432
+ return 1.57 * ( cdi . q3 - cdi . q1 ) / Math . sqrt ( N ) ;
433
+ }
0 commit comments