Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding box plots drawn on std-deviation instead of quartiles #6698

Merged
merged 21 commits into from
Aug 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions draftlogs/6697_add.md
Original file line number Diff line number Diff line change
@@ -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)]
34 changes: 33 additions & 1 deletion src/traces/box/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,30 @@ module.exports = {
'right (left) for vertical boxes and above (below) for horizontal boxes'
].join(' ')
},

sdmultiple: {
valType: 'number',
min: 0,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the minimum be one instead of zero?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if you want 0.5σ? Might be weird but I don't see a reason to prohibit it.

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],
Expand Down Expand Up @@ -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,

Expand Down
6 changes: 4 additions & 2 deletions src/traces/box/calc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is getting too long.
How about adding agetMean(gd, trace) and move & revise this logic there?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Putting the code into a function would make the code less readable - if readability is the concern here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree - if it's only going to be used here, extracting to a function just means it takes another jump to read and understand the code. @archmoj if your concern is just line length we can break this onto more lines. Or leave it for now and sometime add prettier to our toolchain like we have in various other projects 😉

_(gd, 'mean:'),
lf: _(gd, 'lower fence:'),
uf: _(gd, 'upper fence:')
}
Expand Down
10 changes: 9 additions & 1 deletion src/traces/box/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down
4 changes: 2 additions & 2 deletions src/traces/box/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}

Expand Down
34 changes: 22 additions & 12 deletions src/traces/box/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
);

Expand All @@ -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);
Expand All @@ -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 {
Expand All @@ -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
) :
''
)
);
}
Expand Down
Binary file added test/image/baselines/box_sizemode_sd.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
156 changes: 156 additions & 0 deletions test/image/mocks/box_sizemode_sd.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
{
28raining marked this conversation as resolved.
Show resolved Hide resolved
"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
}
}
22 changes: 22 additions & 0 deletions test/plot-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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": {
Expand Down