diff --git a/src/traces/box/attributes.js b/src/traces/box/attributes.js index 9d5d45247ff..fdd2e6e4d7a 100644 --- a/src/traces/box/attributes.js +++ b/src/traces/box/attributes.js @@ -85,6 +85,27 @@ module.exports = { 'For example, with 1, the whiskers are as wide as the box(es).' ].join(' ') }, + notched: { + valType: 'boolean', + role: 'style', + editType: 'calcIfAutorange', + description: [ + 'Determines whether or not notches should be drawn.' + ].join(' ') + }, + notchwidth: { + valType: 'number', + min: 0, + max: 0.5, + dflt: 0.25, + role: 'style', + editType: 'calcIfAutorange', + description: [ + 'Sets the width of the notches relative to', + 'the box\' width.', + 'For example, with 0, the notches are as wide as the box(es).' + ].join(' ') + }, boxpoints: { valType: 'enumerated', values: ['all', 'outliers', 'suspectedoutliers', false], diff --git a/src/traces/box/calc.js b/src/traces/box/calc.js index 93532067058..688f2ba419f 100644 --- a/src/traces/box/calc.js +++ b/src/traces/box/calc.js @@ -111,6 +111,13 @@ module.exports = function calc(gd, trace) { cdi.lo = 4 * cdi.q1 - 3 * cdi.q3; cdi.uo = 4 * cdi.q3 - 3 * cdi.q1; + + // lower and upper notches ~95% Confidence Intervals for median + var iqr = cdi.q3 - cdi.q1; + var mci = 1.57 * iqr / Math.sqrt(bvLen); + cdi.ln = cdi.med - mci; + cdi.un = cdi.med + mci; + cd.push(cdi); } } diff --git a/src/traces/box/defaults.js b/src/traces/box/defaults.js index ff69dcec01d..0ab1d18c377 100644 --- a/src/traces/box/defaults.js +++ b/src/traces/box/defaults.js @@ -29,6 +29,9 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) { coerce('whiskerwidth'); coerce('boxmean'); + var notched = coerce('notched', traceIn.notchwidth !== undefined); + if(notched) coerce('notchwidth'); + handlePointsDefaults(traceIn, traceOut, coerce, {prefix: 'box'}); } diff --git a/src/traces/box/plot.js b/src/traces/box/plot.js index 6c2864ac1ca..f1121f9982e 100644 --- a/src/traces/box/plot.js +++ b/src/traces/box/plot.js @@ -101,6 +101,8 @@ function plotBoxAndWhiskers(sel, axes, trace, t) { var wdPos = t.wdPos || 0; var bPosPxOffset = t.bPosPxOffset || 0; var whiskerWidth = trace.whiskerwidth || 0; + var notched = trace.notched || false; + var nw = notched ? 1 - 2 * trace.notchwidth : 1; // to support for one-sided box var bdPos0; @@ -125,6 +127,8 @@ function plotBoxAndWhiskers(sel, axes, trace, t) { var pos1 = posAxis.c2p(pos + bPos + bdPos1, true) + bPosPxOffset; var posw0 = posAxis.c2p(pos + bPos - wdPos, true) + bPosPxOffset; var posw1 = posAxis.c2p(pos + bPos + wdPos, true) + bPosPxOffset; + var posm0 = posAxis.c2p(pos + bPos - bdPos0 * nw, true) + bPosPxOffset; + var posm1 = posAxis.c2p(pos + bPos + bdPos1 * nw, true) + bPosPxOffset; var q1 = valAxis.c2p(d.q1, true); var q3 = valAxis.c2p(d.q3, true); // make sure median isn't identical to either of the @@ -135,18 +139,30 @@ function plotBoxAndWhiskers(sel, axes, trace, t) { ); var lf = valAxis.c2p(trace.boxpoints === false ? d.min : d.lf, true); var uf = valAxis.c2p(trace.boxpoints === false ? d.max : d.uf, true); + var ln = valAxis.c2p(d.ln, true); + var un = valAxis.c2p(d.un, true); if(trace.orientation === 'h') { d3.select(this).attr('d', - 'M' + m + ',' + pos0 + 'V' + pos1 + // median line - 'M' + q1 + ',' + pos0 + 'V' + pos1 + 'H' + q3 + 'V' + pos0 + 'Z' + // box + 'M' + m + ',' + posm0 + 'V' + posm1 + // median line + 'M' + q1 + ',' + pos0 + 'V' + pos1 + // left edge + (notched ? 'H' + ln + 'L' + m + ',' + posm1 + 'L' + un + ',' + pos1 : '') + // top notched edge + 'H' + q3 + // end of the top edge + '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)); } else { d3.select(this).attr('d', - 'M' + pos0 + ',' + m + 'H' + pos1 + // median line - 'M' + pos0 + ',' + q1 + 'H' + pos1 + 'V' + q3 + 'H' + pos0 + 'Z' + // box + 'M' + posm0 + ',' + m + 'H' + posm1 + // median line + 'M' + pos0 + ',' + q1 + 'H' + pos1 + // top of the box + (notched ? 'V' + ln + 'L' + posm1 + ',' + m + 'L' + pos1 + ',' + un : '') + // notched right edge + 'V' + q3 + // end of the right edge + 'H' + pos0 + // bottom of the box + (notched ? 'V' + un + 'L' + posm0 + ',' + m + 'L' + pos0 + ',' + ln : '') + // 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)); diff --git a/test/image/baselines/box_horz_notched.png b/test/image/baselines/box_horz_notched.png new file mode 100644 index 00000000000..b611698da7f Binary files /dev/null and b/test/image/baselines/box_horz_notched.png differ diff --git a/test/image/baselines/box_notched.png b/test/image/baselines/box_notched.png new file mode 100644 index 00000000000..fbb63c19c2d Binary files /dev/null and b/test/image/baselines/box_notched.png differ diff --git a/test/image/mocks/box_horz_notched.json b/test/image/mocks/box_horz_notched.json new file mode 100644 index 00000000000..e8c8053566c --- /dev/null +++ b/test/image/mocks/box_horz_notched.json @@ -0,0 +1,290 @@ +{ + "data":[ + { + "x":[ + 90, + 88, + 55, + 88, + 72, + 100, + 88, + 25, + 92, + 100, + 82, + 82, + 90, + 68, + 85, + 82, + 40, + 100, + 92, + 82, + 55, + 62, + 85, + 100, + 75, + 88, + 78, + 80, + 92, + 100, + 88, + 72, + 95, + 80, + 90, + 72, + 100, + 100, + 75, + 82, + 60, + 90, + 85, + 90, + 38, + 78, + 82, + 100, + 90, + 80, + 80, + 100, + 70, + 100, + 82, + 62, + 92, + 100, + 80, + 100, + 88, + 85 + ], + "line":{ + "color":"#1c9099" + }, + "type":"box", + "name":"Notched = True", + "orientation": "h", + "notched":true + }, + { + "x":[ + 70, + 65, + 85, + 75, + 72, + 75, + 90, + 88, + 85, + 80, + 92, + 85, + 85, + 75, + 72, + 80, + 42, + 80, + 95, + 90, + 62, + 65, + 65, + 82, + 68, + 48, + 57, + 95, + 70, + 100, + 80, + 95, + 78, + 80, + 80, + 85, + 90, + 100, + 52, + 85, + 72, + 70, + 45, + 75, + 85, + 95, + 65, + 70, + 85, + 70, + 85, + 35, + 90, + 95, + 95, + 65, + 62, + 48, + 60, + 85, + 85, + 90, + 70, + 68 + ], + "line":{ + "color":"#1c9099" + }, + "type":"box", + "name":"Notch width = 0.1", + "notched":true, + "orientation": "h", + "notchwidth":0.1 + }, + { + "x":[ + 95, + 75, + 70, + 72, + 52, + 70, + 82, + 90, + 95, + 80, + 68, + 88, + 82, + 52, + 80, + 78, + 57, + 88, + 88, + 100, + 50, + 65, + 78, + 92, + 65, + 50, + 60, + 88, + 100, + 50, + 90, + 70, + 60, + 72, + 75, + 95, + 100, + 45, + 68, + 72, + 45, + 60, + 78, + 85, + 92, + 45, + 68, + 70, + 85, + 82, + 62, + 75, + 100, + 80, + 65, + 52, + 48, + 57, + 100, + 72, + 100, + 80, + 65 + ], + "line":{ + "color":"#1c9099" + }, + "type":"box", + "name":"Notch width = 0", + "notched":true, + "orientation": "h", + "notchwidth":0 + }, + { + "x":[ + 86, + 64, + 86.2, + 91.1, + 86, + 85.3, + 85.30000000000001, + 63.7, + 47.400000000000006, + 89.2, + 91.3, + 91.6, + 55.099999999999994, + 64.1, + 76.2, + 91.4, + 68.9, + 60.8, + 64.5, + 87.7, + 21, + 97.6, + 90.9, + 86.6, + 100, + 33.6, + 82.7, + 63.599999999999994, + 55.5, + 80.7, + 85, + 92.30000000000001, + 48.9, + 85 + ], + "line":{ + "color":"#1c9099" + }, + "type":"box", + "name":"Outside of hinges", + "orientation": "h", + "notched":true + } + ], + "layout":{ + "showlegend":false, + "xaxis":{ + "title":"Grade [%]", + "type":"linear" + }, + "title":"Based on Fig 4.4a: Course Grade Distributions", + "yaxis":{ + "type":"category" + }, + "height":598, + "width":1080, + "margin":{ + "l":110 + }, + "autosize":true + } +} diff --git a/test/image/mocks/box_notched.json b/test/image/mocks/box_notched.json new file mode 100644 index 00000000000..763c09915fb --- /dev/null +++ b/test/image/mocks/box_notched.json @@ -0,0 +1,282 @@ +{ + "data":[ + { + "y":[ + 90, + 88, + 55, + 88, + 72, + 100, + 88, + 25, + 92, + 100, + 82, + 82, + 90, + 68, + 85, + 82, + 40, + 100, + 92, + 82, + 55, + 62, + 85, + 100, + 75, + 88, + 78, + 80, + 92, + 100, + 88, + 72, + 95, + 80, + 90, + 72, + 100, + 100, + 75, + 82, + 60, + 90, + 85, + 90, + 38, + 78, + 82, + 100, + 90, + 80, + 80, + 100, + 70, + 100, + 82, + 62, + 92, + 100, + 80, + 100, + 88, + 85 + ], + "line":{ + "color":"#1c9099" + }, + "type":"box", + "name":"Notched = True", + "notched":true + }, + { + "y":[ + 70, + 65, + 85, + 75, + 72, + 75, + 90, + 88, + 85, + 80, + 92, + 85, + 85, + 75, + 72, + 80, + 42, + 80, + 95, + 90, + 62, + 65, + 65, + 82, + 68, + 48, + 57, + 95, + 70, + 100, + 80, + 95, + 78, + 80, + 80, + 85, + 90, + 100, + 52, + 85, + 72, + 70, + 45, + 75, + 85, + 95, + 65, + 70, + 85, + 70, + 85, + 35, + 90, + 95, + 95, + 65, + 62, + 48, + 60, + 85, + 85, + 90, + 70, + 68 + ], + "line":{ + "color":"#1c9099" + }, + "type":"box", + "name":"Notch width = 0.1", + "notchwidth":0.1 + }, + { + "y":[ + 95, + 75, + 70, + 72, + 52, + 70, + 82, + 90, + 95, + 80, + 68, + 88, + 82, + 52, + 80, + 78, + 57, + 88, + 88, + 100, + 50, + 65, + 78, + 92, + 65, + 50, + 60, + 88, + 100, + 50, + 90, + 70, + 60, + 72, + 75, + 95, + 100, + 45, + 68, + 72, + 45, + 60, + 78, + 85, + 92, + 45, + 68, + 70, + 85, + 82, + 62, + 75, + 100, + 80, + 65, + 52, + 48, + 57, + 100, + 72, + 100, + 80, + 65 + ], + "line":{ + "color":"#1c9099" + }, + "type":"box", + "name":"Notch width = 0", + "notched":true, + "notchwidth":0 + }, + { + "y":[ + 86, + 64, + 86.2, + 91.1, + 86, + 85.3, + 85.30000000000001, + 63.7, + 47.400000000000006, + 89.2, + 91.3, + 91.6, + 55.099999999999994, + 64.1, + 76.2, + 91.4, + 68.9, + 60.8, + 64.5, + 87.7, + 21, + 97.6, + 90.9, + 86.6, + 100, + 33.6, + 82.7, + 63.599999999999994, + 55.5, + 80.7, + 85, + 92.30000000000001, + 48.9, + 85 + ], + "line":{ + "color":"#1c9099" + }, + "type":"box", + "name":"Outside of hinges", + "notched":true + } + ], + "layout":{ + "showlegend":false, + "yaxis":{ + "title":"Grade [%]", + "type":"linear" + }, + "title":"Based on Fig 4.4a: Course Grade Distributions", + "xaxis":{ + "type":"category" + }, + "height":598, + "width":1080, + "autosize":true + } +}