Skip to content

Commit bc9d121

Browse files
committedDec 18, 2019
add q1/median/q3 defaults + calc logic & tests
1 parent d32f662 commit bc9d121

File tree

6 files changed

+1257
-136
lines changed

6 files changed

+1257
-136
lines changed
 

Diff for: ‎src/plots/cartesian/type_defaults.js

+10-2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ function setAutoType(ax, data) {
4040

4141
var id = ax._id;
4242
var axLetter = id.charAt(0);
43+
var i;
4344

4445
// support 3d
4546
if(id.indexOf('scene') !== -1) id = axLetter;
@@ -50,15 +51,22 @@ function setAutoType(ax, data) {
5051
// first check for histograms, as the count direction
5152
// should always default to a linear axis
5253
if(d0.type === 'histogram' &&
53-
axLetter === {v: 'y', h: 'x'}[d0.orientation || 'v']) {
54+
axLetter === {v: 'y', h: 'x'}[d0.orientation || 'v']
55+
) {
5456
ax.type = 'linear';
5557
return;
5658
}
5759

5860
var calAttr = axLetter + 'calendar';
5961
var calendar = d0[calAttr];
6062
var opts = {noMultiCategory: !traceIs(d0, 'cartesian') || traceIs(d0, 'noMultiCategory')};
61-
var i;
63+
64+
// To not confuse 2D x/y used for per-box sample points for multicategory coordinates
65+
if(d0.type === 'box' && d0._hasPreCompStats &&
66+
axLetter === {h: 'x', v: 'y'}[d0.orientation || 'v']
67+
) {
68+
opts.noMultiCategory = true;
69+
}
6270

6371
// check all boxes on this x axis to see
6472
// if they're dates, numbers, or categories

Diff for: ‎src/traces/box/calc.js

+272-113
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@
1010

1111
var isNumeric = require('fast-isnumeric');
1212

13+
var Axes = require('../../plots/cartesian/axes');
1314
var Lib = require('../../lib');
15+
16+
var BADNUM = require('../../constants/numerical').BADNUM;
1417
var _ = Lib._;
15-
var Axes = require('../../plots/cartesian/axes');
1618

17-
// outlier definition based on http://www.physics.csbsju.edu/stats/box2.html
1819
module.exports = function calc(gd, trace) {
1920
var fullLayout = gd._fullLayout;
2021
var xa = Axes.getFromId(gd, trace.xaxis || 'x');
@@ -24,7 +25,7 @@ module.exports = function calc(gd, trace) {
2425
// N.B. violin reuses same Box.calc
2526
var numKey = trace.type === 'violin' ? '_numViolins' : '_numBoxes';
2627

27-
var i;
28+
var i, j;
2829
var valAxis, valLetter;
2930
var posAxis, posLetter;
3031

@@ -40,127 +41,230 @@ module.exports = function calc(gd, trace) {
4041
posLetter = 'x';
4142
}
4243

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);
4746
var posDistinct = dv.vals;
4847
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-
}
6648

49+
// item in trace calcdata
6750
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
6864
var ptFilterFn = (trace.boxpoints || trace.points) === 'all' ?
6965
Lib.identity :
7066
function(pt) { return (pt.v < cdi.lf || pt.v > cdi.uf); };
7167

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;
7473

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;
8177

8278
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+
}
11294
}
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;
116143
} 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;
119165
}
120166

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);
150169

151170
cdi.pts2 = pts.filter(ptFilterFn);
152171

153172
cd.push(cdi);
154173
}
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+
);
155264
}
156265

157266
calcSelection(cd, trace);
158267

159-
trace._extremes[valAxis._id] = Axes.findExtremes(valAxis,
160-
trace.notched ? val.concat([minLowerNotch, maxUpperNotch]) : val,
161-
{padded: true}
162-
);
163-
164268
if(cd.length > 0) {
165269
cd[0].t = {
166270
num: fullLayout[numKey],
@@ -191,14 +295,17 @@ module.exports = function calc(gd, trace) {
191295
// so if you want one box
192296
// per trace, set x0 (y0) to the x (y) value or category for this trace
193297
// (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)) {
196304
return posAxis.makeCalcdata(trace, posLetter);
197305
}
198306

199307
var pos0;
200-
201-
if(posLetter + '0' in trace) {
308+
if(hasPos0) {
202309
pos0 = trace[posLetter + '0'];
203310
} else if('name' in trace && (
204311
posAxis.type === 'category' || (
@@ -218,7 +325,11 @@ function getPos(trace, posLetter, posAxis, val, num) {
218325
posAxis.r2c_just_indices(pos0) :
219326
posAxis.d2c(pos0, 0, trace[posLetter + 'calendar']);
220327

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;
222333
}
223334

224335
function makeBins(x, dx) {
@@ -241,15 +352,21 @@ function initNestedArray(len) {
241352
return arr;
242353
}
243354

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+
};
249359

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+
}
253370
}
254371
}
255372
}
@@ -272,3 +389,45 @@ function calcSelection(cd, trace) {
272389
function sortByVal(a, b) { return a.v - b.v; }
273390

274391
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+
}

Diff for: ‎src/traces/box/defaults.js

+158-21
Original file line numberDiff line numberDiff line change
@@ -22,50 +22,183 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
2222
handleSampleDefaults(traceIn, traceOut, coerce, layout);
2323
if(traceOut.visible === false) return;
2424

25+
var hasPreCompStats = traceOut._hasPreCompStats;
26+
27+
if(hasPreCompStats) {
28+
coerce('lowerfence');
29+
coerce('upperfence');
30+
}
31+
2532
coerce('line.color', (traceIn.marker || {}).color || defaultColor);
2633
coerce('line.width');
2734
coerce('fillcolor', Color.addOpacity(traceOut.line.color, 0.5));
2835

36+
var boxmeanDflt = false;
37+
if(hasPreCompStats) {
38+
var mean = coerce('mean');
39+
var sd = coerce('sd');
40+
if(mean && mean.length) {
41+
boxmeanDflt = true;
42+
if(sd && sd.length) boxmeanDflt = 'sd';
43+
}
44+
}
45+
coerce('boxmean', boxmeanDflt);
46+
2947
coerce('whiskerwidth');
30-
coerce('boxmean');
3148
coerce('width');
3249
coerce('quartilemethod');
3350

34-
var notched = coerce('notched', traceIn.notchwidth !== undefined);
51+
var notchedDflt = false;
52+
if(hasPreCompStats) {
53+
var notchspan = coerce('notchspan');
54+
if(notchspan && notchspan.length) {
55+
notchedDflt = true;
56+
}
57+
} else if(Lib.validate(traceIn.notchwidth, attributes.notchwidth)) {
58+
notchedDflt = true;
59+
}
60+
var notched = coerce('notched', notchedDflt);
3561
if(notched) coerce('notchwidth');
3662

3763
handlePointsDefaults(traceIn, traceOut, coerce, {prefix: 'box'});
3864
}
3965

4066
function handleSampleDefaults(traceIn, traceOut, coerce, layout) {
67+
function getDims(arr) {
68+
var dims = 0;
69+
if(arr && arr.length) {
70+
dims += 1;
71+
if(Lib.isArrayOrTypedArray(arr[0]) && arr[0].length) {
72+
dims += 1;
73+
}
74+
}
75+
return dims;
76+
}
77+
78+
function valid(astr) {
79+
return Lib.validate(traceIn[astr], attributes[astr]);
80+
}
81+
4182
var y = coerce('y');
4283
var x = coerce('x');
43-
var hasX = x && x.length;
84+
85+
var sLen;
86+
if(traceOut.type === 'box') {
87+
var q1 = coerce('q1');
88+
var median = coerce('median');
89+
var q3 = coerce('q3');
90+
91+
traceOut._hasPreCompStats = (
92+
q1 && q1.length &&
93+
median && median.length &&
94+
q3 && q3.length
95+
);
96+
sLen = Math.min(
97+
Lib.minRowLength(q1),
98+
Lib.minRowLength(median),
99+
Lib.minRowLength(q3)
100+
);
101+
}
102+
103+
var yDims = getDims(y);
104+
var xDims = getDims(x);
105+
var yLen = yDims && Lib.minRowLength(y);
106+
var xLen = xDims && Lib.minRowLength(x);
44107

45108
var defaultOrientation, len;
109+
if(traceOut._hasPreCompStats) {
110+
switch(String(xDims) + String(yDims)) {
111+
// no x / no y
112+
case '00':
113+
var setInX = valid('x0') || valid('dx');
114+
var setInY = valid('y0') || valid('dy');
115+
116+
if(setInY && !setInX) {
117+
defaultOrientation = 'h';
118+
} else {
119+
defaultOrientation = 'v';
120+
}
46121

47-
if(y && y.length) {
122+
len = sLen;
123+
break;
124+
// just x
125+
case '10':
126+
defaultOrientation = 'v';
127+
len = Math.min(sLen, xLen);
128+
break;
129+
case '20':
130+
defaultOrientation = 'h';
131+
len = Math.min(sLen, xLen);
132+
break;
133+
// just y
134+
case '01':
135+
defaultOrientation = 'h';
136+
len = Math.min(sLen, yLen);
137+
break;
138+
case '02':
139+
defaultOrientation = 'v';
140+
len = Math.min(sLen, yLen);
141+
break;
142+
// both
143+
case '12':
144+
defaultOrientation = 'v';
145+
len = Math.min(sLen, xLen, yLen);
146+
break;
147+
case '21':
148+
defaultOrientation = 'h';
149+
len = Math.min(sLen, xLen, yLen);
150+
break;
151+
case '11':
152+
// this one is ill-defined
153+
len = 0;
154+
break;
155+
case '22':
156+
// this one case happen on multi-category axes
157+
defaultOrientation = 'v';
158+
len = Math.min(sLen, xLen, yLen);
159+
break;
160+
}
161+
} else if(yDims > 0) {
48162
defaultOrientation = 'v';
49-
if(hasX) {
50-
len = Math.min(Lib.minRowLength(x), Lib.minRowLength(y));
163+
if(xDims > 0) {
164+
len = Math.min(xLen, yLen);
51165
} else {
52-
coerce('x0');
53-
len = Lib.minRowLength(y);
166+
len = Math.min(yLen);
54167
}
55-
} else if(hasX) {
168+
} else if(xDims > 0) {
56169
defaultOrientation = 'h';
57-
coerce('y0');
58-
len = Lib.minRowLength(x);
170+
len = Math.min(xLen);
59171
} else {
172+
len = 0;
173+
}
174+
175+
if(!len) {
60176
traceOut.visible = false;
61177
return;
62178
}
63179
traceOut._length = len;
64180

181+
var orientation = coerce('orientation', defaultOrientation);
182+
183+
// these are just used for positioning, they never define the sample
184+
if(traceOut._hasPreCompStats) {
185+
if(orientation === 'v' && xDims === 0) {
186+
coerce('x0', 0);
187+
coerce('dx', 1);
188+
} else if(orientation === 'h' && yDims === 0) {
189+
coerce('y0', 0);
190+
coerce('dy', 1);
191+
}
192+
} else {
193+
if(orientation === 'v' && xDims === 0) {
194+
coerce('x0');
195+
} else if(orientation === 'h' && yDims === 0) {
196+
coerce('y0');
197+
}
198+
}
199+
65200
var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults');
66201
handleCalendarDefaults(traceIn, traceOut, ['x', 'y'], layout);
67-
68-
coerce('orientation', defaultOrientation);
69202
}
70203

71204
function handlePointsDefaults(traceIn, traceOut, coerce, opts) {
@@ -74,14 +207,18 @@ function handlePointsDefaults(traceIn, traceOut, coerce, opts) {
74207
var outlierColorDflt = Lib.coerce2(traceIn, traceOut, attributes, 'marker.outliercolor');
75208
var lineoutliercolor = coerce('marker.line.outliercolor');
76209

77-
var points = coerce(
78-
prefix + 'points',
79-
(outlierColorDflt || lineoutliercolor) ? 'suspectedoutliers' : undefined
80-
);
210+
var modeDflt = 'outliers';
211+
if(traceOut._hasPreCompStats) {
212+
modeDflt = 'all';
213+
} else if(outlierColorDflt || lineoutliercolor) {
214+
modeDflt = 'suspectedoutliers';
215+
}
216+
217+
var mode = coerce(prefix + 'points', modeDflt);
81218

82-
if(points) {
83-
coerce('jitter', points === 'all' ? 0.3 : 0);
84-
coerce('pointpos', points === 'all' ? -1.5 : 0);
219+
if(mode) {
220+
coerce('jitter', mode === 'all' ? 0.3 : 0);
221+
coerce('pointpos', mode === 'all' ? -1.5 : 0);
85222

86223
coerce('marker.symbol');
87224
coerce('marker.opacity');
@@ -90,7 +227,7 @@ function handlePointsDefaults(traceIn, traceOut, coerce, opts) {
90227
coerce('marker.line.color');
91228
coerce('marker.line.width');
92229

93-
if(points === 'suspectedoutliers') {
230+
if(mode === 'suspectedoutliers') {
94231
coerce('marker.line.outliercolor', traceOut.marker.color);
95232
coerce('marker.line.outlierwidth');
96233
}

Diff for: ‎test/image/baselines/box_precomputed-stats.png

50.5 KB
Loading

Diff for: ‎test/image/mocks/box_precomputed-stats.json

+211
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
{
2+
"data": [
3+
{
4+
"type": "box",
5+
"name": "[V] just q1/median/q3",
6+
"offsetgroup": "1",
7+
"q1": [ 1, 2, 1 ],
8+
"median": [ 2, 3, 2 ],
9+
"q3": [ 3, 4, 3 ]
10+
},
11+
{
12+
"type": "box",
13+
"name": "[V] q1/median/q3/lowerfence/upperfence",
14+
"offsetgroup": "2",
15+
"q1": [ 1, 2, 1 ],
16+
"median": [ 2, 3, 2 ],
17+
"q3": [ 3, 4, 3 ],
18+
"lowerfence": [ 0, 1, 0 ],
19+
"upperfence": [ 4, 5, 4 ]
20+
},
21+
{
22+
"type": "box",
23+
"name": "[V] all pre-computed stats",
24+
"offsetgroup": "3",
25+
"q1": [ 1, 2, 1 ],
26+
"median": [ 2, 3, 2 ],
27+
"q3": [ 3, 4, 3 ],
28+
"lowerfence": [ 0, 1, 0 ],
29+
"upperfence": [ 4, 5, 4 ],
30+
"mean": [ 2.2, 2.8, 2.2 ],
31+
"sd": [ 0.4, 0.4, 0.4 ],
32+
"notchspan": [ 0.2, 0.1, 0.2 ]
33+
},
34+
{
35+
"type": "box",
36+
"name": "[V] set q1/median/q3 computed lowerfence/upperfence",
37+
"offsetgroup": "1",
38+
"q1": [ 1, 2, 1 ],
39+
"median": [ 2, 3, 2 ],
40+
"q3": [ 3, 4, 3 ],
41+
"y": [
42+
[ 0, 1, 2, 3, 4 ],
43+
[ 1, 2, 3, 4, 5 ],
44+
[ 0, 1, 2, 3, 4 ]
45+
],
46+
"xaxis": "x2",
47+
"yaxis": "y2"
48+
},
49+
{
50+
"type": "box",
51+
"name": "[V] set q1/median/q3/lowerfence/upperfence computed mean",
52+
"offsetgroup": "2",
53+
"q1": [ 1, 2, 1 ],
54+
"median": [ 2, 3, 2 ],
55+
"q3": [ 3, 4, 3 ],
56+
"lowerfence": [ -1, 0, -1 ],
57+
"upperfence": [ 5, 6, 5 ],
58+
"boxmean": true,
59+
"y": [
60+
[ 0, 1, 2, 3, 4 ],
61+
[ 1, 2, 3, 4, 5, 8 ],
62+
[ 0, 1, 2, 3, 4 ]
63+
],
64+
"xaxis": "x2",
65+
"yaxis": "y2"
66+
},
67+
{
68+
"type": "box",
69+
"name": "[V] set q1/median/q3 computed lowerfence/upperfence/mean/sd/notches",
70+
"offsetgroup": "3",
71+
"q1": [ 1, 2, 1 ],
72+
"median": [ 2, 3, 2 ],
73+
"q3": [ 3, 4, 3 ],
74+
"y": [
75+
[ 0, 1, 2, 3, 4 ],
76+
[ 1, 2, 3, 4, 5 ],
77+
[ 0, 1, 2, 3, 4 ]
78+
],
79+
"boxmean": "sd",
80+
"notched": true,
81+
"xaxis": "x2",
82+
"yaxis": "y2"
83+
},
84+
{
85+
"type": "box",
86+
"name": "[H] just q1/median/q3",
87+
"offsetgroup": "1",
88+
"y0": 1,
89+
"q1": [ 1, 2, 1 ],
90+
"median": [ 2, 3, 2 ],
91+
"q3": [ 3, 4, 3 ],
92+
"xaxis": "x3",
93+
"yaxis": "y3"
94+
},
95+
{
96+
"type": "box",
97+
"name": "[H] q1/median/q3/lowerfence/upperfence",
98+
"offsetgroup": "2",
99+
"y0": 1,
100+
"q1": [ 1, 2, 1 ],
101+
"median": [ 2, 3, 2 ],
102+
"q3": [ 3, 4, 3 ],
103+
"lowerfence": [ 0, 1, 0 ],
104+
"upperfence": [ 4, 5, 4 ],
105+
"xaxis": "x3",
106+
"yaxis": "y3"
107+
},
108+
{
109+
"type": "box",
110+
"name": "[H] all pre-computed stats",
111+
"offsetgroup": "3",
112+
"y0": 1,
113+
"q1": [ 1, 2, 1 ],
114+
"median": [ 2, 3, 2 ],
115+
"q3": [ 3, 4, 3 ],
116+
"lowerfence": [ 0, 1, 0 ],
117+
"upperfence": [ 4, 5, 4 ],
118+
"mean": [ 2.2, 2.8, 2.2 ],
119+
"sd": [ 0.4, 0.4, 0.4 ],
120+
"notchspan": [ 0.2, 0.1, 0.2 ],
121+
"xaxis": "x3",
122+
"yaxis": "y3"
123+
},
124+
{
125+
"type": "box",
126+
"name": "[H] set q1/median/q3 computed lowerfence/upperfence",
127+
"offsetgroup": "1",
128+
"q1": [ 1, 2, 1 ],
129+
"median": [ 2, 3, 2 ],
130+
"q3": [ 3, 4, 3 ],
131+
"x": [
132+
[ 0, 1, 2, 3, 4 ],
133+
[ 1, 2, 3, 4, 5 ],
134+
[ 0, 1, 2, 3, 4 ]
135+
],
136+
"xaxis": "x4",
137+
"yaxis": "y4"
138+
},
139+
{
140+
"type": "box",
141+
"name": "[H] set q1/median/q3/lowerfence/upperfence computed mean",
142+
"offsetgroup": "2",
143+
"q1": [ 1, 2, 1 ],
144+
"median": [ 2, 3, 2 ],
145+
"q3": [ 3, 4, 3 ],
146+
"lowerfence": [ -1, 0, -1 ],
147+
"upperfence": [ 5, 6, 5 ],
148+
"boxmean": true,
149+
"x": [
150+
[ 0, 1, 2, 3, 4 ],
151+
[ 1, 2, 3, 4, 5, 8 ],
152+
[ 0, 1, 2, 3, 4 ]
153+
],
154+
"xaxis": "x4",
155+
"yaxis": "y4"
156+
},
157+
{
158+
"type": "box",
159+
"name": "[H] set q1/median/q3 computed lowerfence/upperfence/mean/sd/notches",
160+
"offsetgroup": "3",
161+
"q1": [ 1, 2, 1 ],
162+
"median": [ 2, 3, 2 ],
163+
"q3": [ 3, 4, 3 ],
164+
"x": [
165+
[ 0, 1, 2, 3, 4 ],
166+
[ 1, 2, 3, 4, 5 ],
167+
[ 0, 1, 2, 3, 4 ]
168+
],
169+
"boxmean": "sd",
170+
"notched": true,
171+
"xaxis": "x4",
172+
"yaxis": "y4"
173+
}
174+
],
175+
"layout": {
176+
"showlegend": false,
177+
"boxmode": "group",
178+
"yaxis": {
179+
"domain": [ 0.78, 1 ]
180+
},
181+
"xaxis2": {
182+
"anchor": "y2"
183+
},
184+
"yaxis2": {
185+
"domain": [ 0.51, 0.72 ],
186+
"anchor": "x2"
187+
},
188+
"xaxis3": {
189+
"domain": [ 0, 0.5 ],
190+
"anchor": "y3"
191+
},
192+
"yaxis3": {
193+
"domain": [ 0, 0.48 ],
194+
"anchor": "x3"
195+
},
196+
"xaxis4": {
197+
"domain": [ 0.5, 1 ],
198+
"anchor": "y4"
199+
},
200+
"yaxis4": {
201+
"domain": [ 0, 0.48 ],
202+
"anchor": "x4"
203+
},
204+
"margin": { "l": 20, "t": 40, "b": 20, "r": 20 },
205+
"title": {
206+
"text": "box traces with pre-computed stats",
207+
"x": 0
208+
},
209+
"hovermode": "closest"
210+
}
211+
}

Diff for: ‎test/jasmine/tests/box_test.js

+606
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.