diff --git a/draftlogs/6697_add.md b/draftlogs/6697_add.md new file mode 100644 index 00000000000..17a8170daa2 --- /dev/null +++ b/draftlogs/6697_add.md @@ -0,0 +1 @@ +- Add [1-6]-sigma (std deviations) box plots as an alternative to quartiles [[#6697](https://github.com/plotly/plotly.js/issues/6697)] \ No newline at end of file diff --git a/src/traces/box/attributes.js b/src/traces/box/attributes.js index 81fdce06524..95c8a80e538 100644 --- a/src/traces/box/attributes.js +++ b/src/traces/box/attributes.js @@ -218,7 +218,30 @@ module.exports = { 'right (left) for vertical boxes and above (below) for horizontal boxes' ].join(' ') }, - + sdmultiple: { + valType: 'number', + min: 0, + editType: 'calc', + dflt: 1, + description: [ + 'Scales the box size when sizemode=sd', + 'Allowing boxes to be drawn across any stddev range', + 'For example 1-stddev, 3-stddev, 5-stddev', + ].join(' ') + }, + sizemode: { + valType: 'enumerated', + values: ['quartiles', 'sd'], + editType: 'calc', + dflt: 'quartiles', + description: [ + 'Sets the upper and lower bound for the boxes', + 'quartiles means box is drawn between Q1 and Q3', + 'SD means the box is drawn between Mean +- Standard Deviation', + 'Argument sdmultiple (default 1) to scale the box size', + 'So it could be drawn 1-stddev, 3-stddev etc', + ].join(' ') + }, boxmean: { valType: 'enumerated', values: [true, 'sd', false], @@ -378,6 +401,15 @@ module.exports = { ].join(' ') }, + showwhiskers: { + valType: 'boolean', + editType: 'calc', + description: [ + 'Determines whether or not whiskers are visible.', + 'Defaults to true for `sizemode` *quartiles*, false for *sd*.' + ].join(' ') + }, + offsetgroup: barAttrs.offsetgroup, alignmentgroup: barAttrs.alignmentgroup, diff --git a/src/traces/box/calc.js b/src/traces/box/calc.js index 83eadb0b1f8..cbb045d189e 100644 --- a/src/traces/box/calc.js +++ b/src/traces/box/calc.js @@ -221,7 +221,7 @@ module.exports = function calc(gd, trace) { cdi.min = boxVals[0]; cdi.max = boxVals[N - 1]; cdi.mean = Lib.mean(boxVals, N); - cdi.sd = Lib.stdev(boxVals, N, cdi.mean); + cdi.sd = Lib.stdev(boxVals, N, cdi.mean) * trace.sdmultiple; cdi.med = Lib.interp(boxVals, 0.5); if((N % 2) && (usesExclusive || usesInclusive)) { @@ -286,7 +286,9 @@ module.exports = function calc(gd, trace) { q1: _(gd, 'q1:'), q3: _(gd, 'q3:'), max: _(gd, 'max:'), - mean: trace.boxmean === 'sd' ? _(gd, 'mean ± σ:') : _(gd, 'mean:'), + mean: (trace.boxmean === 'sd') || (trace.sizemode === 'sd') ? + _(gd, 'mean ± σ:').replace('σ', trace.sdmultiple === 1 ? 'σ' : (trace.sdmultiple + 'σ')) : // displaying mean +- Nσ whilst supporting translations + _(gd, 'mean:'), lf: _(gd, 'lower fence:'), uf: _(gd, 'upper fence:') } diff --git a/src/traces/box/defaults.js b/src/traces/box/defaults.js index f2f087b714a..892d4401bd0 100644 --- a/src/traces/box/defaults.js +++ b/src/traces/box/defaults.js @@ -40,9 +40,17 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) { if(sd && sd.length) boxmeanDflt = 'sd'; } } - coerce('boxmean', boxmeanDflt); coerce('whiskerwidth'); + var sizemode = coerce('sizemode'); + var boxmean; + if(sizemode === 'quartiles') { + boxmean = coerce('boxmean', boxmeanDflt); + } + coerce('showwhiskers', sizemode === 'quartiles'); + if((sizemode === 'sd') || (boxmean === 'sd')) { + coerce('sdmultiple'); + } coerce('width'); coerce('quartilemethod'); diff --git a/src/traces/box/hover.js b/src/traces/box/hover.js index 479d434f42c..8eddfe96235 100644 --- a/src/traces/box/hover.js +++ b/src/traces/box/hover.js @@ -140,7 +140,7 @@ function hoverOnBoxes(pointData, xval, yval, hovermode) { pointData.spikeDistance = dxy(di) * spikePseudoDistance / hoverPseudoDistance; pointData[spikePosAttr] = pAxis.c2p(di.pos, true); - var hasMean = trace.boxmean || (trace.meanline || {}).visible; + var hasMean = trace.boxmean || (trace.sizemode === 'sd') || (trace.meanline || {}).visible; var hasFences = trace.boxpoints || trace.points; // labels with equal values (e.g. when min === q1) should still be presented in the order they have when they're unequal @@ -179,7 +179,7 @@ function hoverOnBoxes(pointData, xval, yval, hovermode) { // clicked point from a box during click-to-select pointData2.hoverOnBox = true; - if(attr === 'mean' && ('sd' in di) && trace.boxmean === 'sd') { + if(attr === 'mean' && ('sd' in di) && ((trace.boxmean === 'sd') || (trace.sizemode === 'sd'))) { pointData2[vLetter + 'err'] = di.sd; } diff --git a/src/traces/box/plot.js b/src/traces/box/plot.js index 3fd80ce070e..3249457c8c5 100644 --- a/src/traces/box/plot.js +++ b/src/traces/box/plot.js @@ -54,6 +54,7 @@ function plotBoxAndWhiskers(sel, axes, trace, t, isStatic) { var wdPos = t.wdPos || 0; var bPosPxOffset = t.bPosPxOffset || 0; var whiskerWidth = trace.whiskerwidth || 0; + var showWhiskers = (trace.showwhiskers !== false); var notched = trace.notched || false; var nw = notched ? 1 - 2 * trace.notchwidth : 1; @@ -94,12 +95,15 @@ function plotBoxAndWhiskers(sel, axes, trace, t, isStatic) { var posm0 = posAxis.l2p(lcenter - bdPos0 * nw) + bPosPxOffset; var posm1 = posAxis.l2p(lcenter + bdPos1 * nw) + bPosPxOffset; - var q1 = valAxis.c2p(d.q1, true); - var q3 = valAxis.c2p(d.q3, true); + var sdmode = trace.sizemode === 'sd'; + var q1 = valAxis.c2p(sdmode ? d.mean - d.sd : d.q1, true); + var q3 = sdmode ? valAxis.c2p(d.mean + d.sd, true) : + valAxis.c2p(d.q3, true); // make sure median isn't identical to either of the // quartiles, so we can see it var m = Lib.constrain( - valAxis.c2p(d.med, true), + sdmode ? valAxis.c2p(d.mean, true) : + valAxis.c2p(d.med, true), Math.min(q1, q3) + 1, Math.max(q1, q3) - 1 ); @@ -109,7 +113,7 @@ function plotBoxAndWhiskers(sel, axes, trace, t, isStatic) { // - box always has d.lf, but boxpoints can be anything // - violin has d.lf and should always use it (boxpoints is undefined) // - candlestick has only min/max - var useExtremes = (d.lf === undefined) || (trace.boxpoints === false); + var useExtremes = (d.lf === undefined) || (trace.boxpoints === false) || sdmode; var lf = valAxis.c2p(useExtremes ? d.min : d.lf, true); var uf = valAxis.c2p(useExtremes ? d.max : d.uf, true); var ln = valAxis.c2p(d.ln, true); @@ -127,10 +131,13 @@ function plotBoxAndWhiskers(sel, axes, trace, t, isStatic) { 'V' + pos0 + // right edge (notched ? 'H' + un + 'L' + m + ',' + posm0 + 'L' + ln + ',' + pos0 : '') + // bottom notched edge 'Z' + // end of the box - 'M' + q1 + ',' + posc + 'H' + lf + 'M' + q3 + ',' + posc + 'H' + uf + // whiskers - (whiskerWidth === 0 ? - '' : // whisker caps - 'M' + lf + ',' + posw0 + 'V' + posw1 + 'M' + uf + ',' + posw0 + 'V' + posw1 + (showWhiskers ? + 'M' + q1 + ',' + posc + 'H' + lf + 'M' + q3 + ',' + posc + 'H' + uf + // whiskers + (whiskerWidth === 0 ? + '' : // whisker caps + 'M' + lf + ',' + posw0 + 'V' + posw1 + 'M' + uf + ',' + posw0 + 'V' + posw1 + ) : + '' ) ); } else { @@ -148,10 +155,13 @@ function plotBoxAndWhiskers(sel, axes, trace, t, isStatic) { '' ) + // notched left edge 'Z' + // end of the box - 'M' + posc + ',' + q1 + 'V' + lf + 'M' + posc + ',' + q3 + 'V' + uf + // whiskers - (whiskerWidth === 0 ? - '' : // whisker caps - 'M' + posw0 + ',' + lf + 'H' + posw1 + 'M' + posw0 + ',' + uf + 'H' + posw1 + (showWhiskers ? + 'M' + posc + ',' + q1 + 'V' + lf + 'M' + posc + ',' + q3 + 'V' + uf + // whiskers + (whiskerWidth === 0 ? + '' : // whisker caps + 'M' + posw0 + ',' + lf + 'H' + posw1 + 'M' + posw0 + ',' + uf + 'H' + posw1 + ) : + '' ) ); } diff --git a/test/image/baselines/box_sizemode_sd.png b/test/image/baselines/box_sizemode_sd.png new file mode 100644 index 00000000000..38f0999a4ca Binary files /dev/null and b/test/image/baselines/box_sizemode_sd.png differ diff --git a/test/image/mocks/box_sizemode_sd.json b/test/image/mocks/box_sizemode_sd.json new file mode 100644 index 00000000000..cc0b711518f --- /dev/null +++ b/test/image/mocks/box_sizemode_sd.json @@ -0,0 +1,156 @@ +{ + "data": [ + { + "y": [ + 0.4995, 0.3786, 0.5188, 0.4505, 0.5805, 0.5539, 0.7341, 0.3303, 0.6202, 0.4754, 0.4814, 0.6103, 0.5157, 0.3841, 0.6166, 0.4917, 0.5646, 0.4532, 0.5983, + 0.5079, 0.5227, 0.5483, 0.3081, 0.5172, 0.4127, 0.3871, 0.4703, 0.5331, 0.4573, 0.6187, 0.5718, 0.4474, 0.5349, 0.4614, 0.3096, 0.4411, 0.7143, 0.5956, + 0.5171, 0.5515, 0.4234, 0.4589, 0.4289, 0.417, 0.4229, 0.5039, 0.4726, 0.5382, 0.261, 0.5326, 0.4472, 0.54, 0.4998, 0.5513, 0.3891, 0.4821, 0.5006, + 0.4362, 0.4184, 0.3632, 0.5182, 0.5229, 0.5493, 0.507, 0.4528, 0.4836, 0.5009, 0.49, 0.5757, 0.5408, 0.4246, 0.511, 0.5216, 0.3817, 0.4471, 0.4935, + 0.5668, 0.3187, 0.3633, 0.5964, 0.637, 0.4701, 0.4292, 0.4921, 0.439, 0.5846, 0.4502, 0.44, 0.3057, 0.4756, 0.4867, 0.502, 0.438, 0.6819, 0.2607, + 0.5233, 0.3291, 0.5555, 0.5397, 0.5068 + ], + "line": { + "color": "#1c9099" + }, + "type": "box", + "boxpoints": "all", + "pointpos": 0, + "name": "1-sigma", + "sizemode": "sd", + "showwhiskers": false + }, + { + "y": [ + 0.4995, 0.3786, 0.5188, 0.4505, 0.5805, 0.5539, 0.7341, 0.3303, 0.6202, 0.4754, 0.4814, 0.6103, 0.5157, 0.3841, 0.6166, 0.4917, 0.5646, 0.4532, 0.5983, + 0.5079, 0.5227, 0.5483, 0.3081, 0.5172, 0.4127, 0.3871, 0.4703, 0.5331, 0.4573, 0.6187, 0.5718, 0.4474, 0.5349, 0.4614, 0.3096, 0.4411, 0.7143, 0.5956, + 0.5171, 0.5515, 0.4234, 0.4589, 0.4289, 0.417, 0.4229, 0.5039, 0.4726, 0.5382, 0.261, 0.5326, 0.4472, 0.54, 0.4998, 0.5513, 0.3891, 0.4821, 0.5006, + 0.4362, 0.4184, 0.3632, 0.5182, 0.5229, 0.5493, 0.507, 0.4528, 0.4836, 0.5009, 0.49, 0.5757, 0.5408, 0.4246, 0.511, 0.5216, 0.3817, 0.4471, 0.4935, + 0.5668, 0.3187, 0.3633, 0.5964, 0.637, 0.4701, 0.4292, 0.4921, 0.439, 0.5846, 0.4502, 0.44, 0.3057, 0.4756, 0.4867, 0.502, 0.438, 0.6819, 0.2607, + 0.5233, 0.3291, 0.5555, 0.5397, 0.5068 + ], + "line": { + "color": "#1c9099" + }, + "boxpoints": "all", + "pointpos": 0, + "type": "box", + "name": "2-sigma", + "sdmultiple" : 2, + "sizemode": "sd", + "showwhiskers": false + }, + { + "y": [ + 0.4995, 0.3786, 0.5188, 0.4505, 0.5805, 0.5539, 0.7341, 0.3303, 0.6202, 0.4754, 0.4814, 0.6103, 0.5157, 0.3841, 0.6166, 0.4917, 0.5646, 0.4532, 0.5983, + 0.5079, 0.5227, 0.5483, 0.3081, 0.5172, 0.4127, 0.3871, 0.4703, 0.5331, 0.4573, 0.6187, 0.5718, 0.4474, 0.5349, 0.4614, 0.3096, 0.4411, 0.7143, 0.5956, + 0.5171, 0.5515, 0.4234, 0.4589, 0.4289, 0.417, 0.4229, 0.5039, 0.4726, 0.5382, 0.261, 0.5326, 0.4472, 0.54, 0.4998, 0.5513, 0.3891, 0.4821, 0.5006, + 0.4362, 0.4184, 0.3632, 0.5182, 0.5229, 0.5493, 0.507, 0.4528, 0.4836, 0.5009, 0.49, 0.5757, 0.5408, 0.4246, 0.511, 0.5216, 0.3817, 0.4471, 0.4935, + 0.5668, 0.3187, 0.3633, 0.5964, 0.637, 0.4701, 0.4292, 0.4921, 0.439, 0.5846, 0.4502, 0.44, 0.3057, 0.4756, 0.4867, 0.502, 0.438, 0.6819, 0.2607, + 0.5233, 0.3291, 0.5555, 0.5397, 0.5068 + ], + "line": { + "color": "#1c9099" + }, + "type": "box", + "boxpoints": "all", + "pointpos": 0, + "name": "3-sigma", + "sdmultiple" : 3, + "sizemode": "sd", + "showwhiskers": false + }, + { + "y": [ + 0.4995, 0.3786, 0.5188, 0.4505, 0.5805, 0.5539, 0.7341, 0.3303, 0.6202, 0.4754, 0.4814, 0.6103, 0.5157, 0.3841, 0.6166, 0.4917, 0.5646, 0.4532, 0.5983, + 0.5079, 0.5227, 0.5483, 0.3081, 0.5172, 0.4127, 0.3871, 0.4703, 0.5331, 0.4573, 0.6187, 0.5718, 0.4474, 0.5349, 0.4614, 0.3096, 0.4411, 0.7143, 0.5956, + 0.5171, 0.5515, 0.4234, 0.4589, 0.4289, 0.417, 0.4229, 0.5039, 0.4726, 0.5382, 0.261, 0.5326, 0.4472, 0.54, 0.4998, 0.5513, 0.3891, 0.4821, 0.5006, + 0.4362, 0.4184, 0.3632, 0.5182, 0.5229, 0.5493, 0.507, 0.4528, 0.4836, 0.5009, 0.49, 0.5757, 0.5408, 0.4246, 0.511, 0.5216, 0.3817, 0.4471, 0.4935, + 0.5668, 0.3187, 0.3633, 0.5964, 0.637, 0.4701, 0.4292, 0.4921, 0.439, 0.5846, 0.4502, 0.44, 0.3057, 0.4756, 0.4867, 0.502, 0.438, 0.6819, 0.2607, + 0.5233, 0.3291, 0.5555, 0.5397, 0.5068 + ], + "line": { + "color": "#1c9099" + }, + "type": "box", + "boxpoints": "all", + "pointpos": 0, + "name": "1-sigma + whiskers", + "sizemode": "sd", + "showwhiskers": true + }, + { + "y": [ + 0.4995, 0.3786, 0.5188, 0.4505, 0.5805, 0.5539, 0.7341, 0.3303, 0.6202, 0.4754, 0.4814, 0.6103, 0.5157, 0.3841, 0.6166, 0.4917, 0.5646, 0.4532, 0.5983, + 0.5079, 0.5227, 0.5483, 0.3081, 0.5172, 0.4127, 0.3871, 0.4703, 0.5331, 0.4573, 0.6187, 0.5718, 0.4474, 0.5349, 0.4614, 0.3096, 0.4411, 0.7143, 0.5956, + 0.5171, 0.5515, 0.4234, 0.4589, 0.4289, 0.417, 0.4229, 0.5039, 0.4726, 0.5382, 0.261, 0.5326, 0.4472, 0.54, 0.4998, 0.5513, 0.3891, 0.4821, 0.5006, + 0.4362, 0.4184, 0.3632, 0.5182, 0.5229, 0.5493, 0.507, 0.4528, 0.4836, 0.5009, 0.49, 0.5757, 0.5408, 0.4246, 0.511, 0.5216, 0.3817, 0.4471, 0.4935, + 0.5668, 0.3187, 0.3633, 0.5964, 0.637, 0.4701, 0.4292, 0.4921, 0.439, 0.5846, 0.4502, 0.44, 0.3057, 0.4756, 0.4867, 0.502, 0.438, 0.6819, 0.2607, + 0.5233, 0.3291, 0.5555, 0.5397, 0.5068 + ], + "line": { + "color": "#1c9099" + }, + "boxpoints": "all", + "pointpos": 0, + "type": "box", + "name": "3-sigma + whiskers", + "sdmultiple" : 3, + "sizemode": "sd", + "showwhiskers": true + }, + { + "y": [ + 0.4995, 0.3786, 0.5188, 0.4505, 0.5805, 0.5539, 0.7341, 0.3303, 0.6202, 0.4754, 0.4814, 0.6103, 0.5157, 0.3841, 0.6166, 0.4917, 0.5646, 0.4532, 0.5983, + 0.5079, 0.5227, 0.5483, 0.3081, 0.5172, 0.4127, 0.3871, 0.4703, 0.5331, 0.4573, 0.6187, 0.5718, 0.4474, 0.5349, 0.4614, 0.3096, 0.4411, 0.7143, 0.5956, + 0.5171, 0.5515, 0.4234, 0.4589, 0.4289, 0.417, 0.4229, 0.5039, 0.4726, 0.5382, 0.261, 0.5326, 0.4472, 0.54, 0.4998, 0.5513, 0.3891, 0.4821, 0.5006, + 0.4362, 0.4184, 0.3632, 0.5182, 0.5229, 0.5493, 0.507, 0.4528, 0.4836, 0.5009, 0.49, 0.5757, 0.5408, 0.4246, 0.511, 0.5216, 0.3817, 0.4471, 0.4935, + 0.5668, 0.3187, 0.3633, 0.5964, 0.637, 0.4701, 0.4292, 0.4921, 0.439, 0.5846, 0.4502, 0.44, 0.3057, 0.4756, 0.4867, 0.502, 0.438, 0.6819, 0.2607, + 0.5233, 0.3291, 0.5555, 0.5397, 0.5068 + ], + "line": { + "color": "#1c9099" + }, + "boxpoints": "all", + "pointpos": 0, + "type": "box", + "name": "diamond 1-sigma", + "boxmean": "sd", + "showwhiskers": false + }, + { + "y": [ + 0.4995, 0.3786, 0.5188, 0.4505, 0.5805, 0.5539, 0.7341, 0.3303, 0.6202, 0.4754, 0.4814, 0.6103, 0.5157, 0.3841, 0.6166, 0.4917, 0.5646, 0.4532, 0.5983, + 0.5079, 0.5227, 0.5483, 0.3081, 0.5172, 0.4127, 0.3871, 0.4703, 0.5331, 0.4573, 0.6187, 0.5718, 0.4474, 0.5349, 0.4614, 0.3096, 0.4411, 0.7143, 0.5956, + 0.5171, 0.5515, 0.4234, 0.4589, 0.4289, 0.417, 0.4229, 0.5039, 0.4726, 0.5382, 0.261, 0.5326, 0.4472, 0.54, 0.4998, 0.5513, 0.3891, 0.4821, 0.5006, + 0.4362, 0.4184, 0.3632, 0.5182, 0.5229, 0.5493, 0.507, 0.4528, 0.4836, 0.5009, 0.49, 0.5757, 0.5408, 0.4246, 0.511, 0.5216, 0.3817, 0.4471, 0.4935, + 0.5668, 0.3187, 0.3633, 0.5964, 0.637, 0.4701, 0.4292, 0.4921, 0.439, 0.5846, 0.4502, 0.44, 0.3057, 0.4756, 0.4867, 0.502, 0.438, 0.6819, 0.2607, + 0.5233, 0.3291, 0.5555, 0.5397, 0.5068 + ], + "line": { + "color": "#1c9099" + }, + "boxpoints": "all", + "pointpos": 0, + "type": "box", + "name": "diamond 3-sigma", + "sdmultiple" : 3, + "boxmean": "sd", + "showwhiskers": false + } + ], + "layout": { + "showlegend": false, + "yaxis": { + "title": { "text": "Random Variable" }, + "type": "linear", + "range":[-0.2, 1.2] + }, + "title": { "text": "Box plots drawn on std deviation instead of quartiles" }, + "xaxis": { + "type": "category" + }, + "height": 598, + "width": 1080, + "autosize": true + } +} diff --git a/test/plot-schema.json b/test/plot-schema.json index 813b0668adc..d9f9e58c7fa 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -16594,6 +16594,13 @@ "editType": "calc", "valType": "data_array" }, + "sdmultiple": { + "description": "Scales the box size when sizemode=sd Allowing boxes to be drawn across any stddev range For example 1-stddev, 3-stddev, 5-stddev", + "dflt": 1, + "editType": "calc", + "min": 0, + "valType": "number" + }, "sdsrc": { "description": "Sets the source reference on Chart Studio Cloud for `sd`.", "editType": "none", @@ -16636,6 +16643,21 @@ "editType": "style", "valType": "boolean" }, + "showwhiskers": { + "description": "Determines whether or not whiskers are visible. Defaults to true for `sizemode` *quartiles*, false for *sd*.", + "editType": "calc", + "valType": "boolean" + }, + "sizemode": { + "description": "Sets the upper and lower bound for the boxes quartiles means box is drawn between Q1 and Q3 SD means the box is drawn between Mean +- Standard Deviation Argument sdmultiple (default 1) to scale the box size So it could be drawn 1-stddev, 3-stddev etc", + "dflt": "quartiles", + "editType": "calc", + "valType": "enumerated", + "values": [ + "quartiles", + "sd" + ] + }, "stream": { "editType": "calc", "maxpoints": {