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

Implement notched box plots, closes #2286 #1

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
22 changes: 22 additions & 0 deletions src/traces/box/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,28 @@ module.exports = {
'For example, with 1, the whiskers are as wide as the box(es).'
].join(' ')
},
notched: {
valType: 'boolean',
dflt: false,
role: 'style',
editType: 'calcIfAutorange',
description: [
'Determines whether or not notches should be drawn.'
].join(' ')
},
notchwidth: {
valType: 'number',
min: 0,
max: 1,
dflt: 0.5,
role: 'style',
editType: 'calcIfAutorange',
description: [
'Sets the width of the notches relative to',
'the box\' width.',
'For example, with 1, the whiskers are as wide as the box(es).'

Choose a reason for hiding this comment

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

It looks like Matlab and ggplot have opposite conventions about this... if I'm reading it right this is the ggplot version. I would have probably gone for the Matlab version, where notchwidth=0 is nothing taken away, and notchwidth=0.5 is notches that meet in the middle (actually I might have called that 1 instead... but we don't want to introduce yet ANOTHER convention). I guess I don't have that strong an opinion about it though, @etpinard or @cpsievert do you care which we use?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Good point; I will wait for others to express their opinion. Just to have it here: the behavior (and description) of notchwidth is based on another, already existing attribute - whiskerwidth.

].join(' ')
},
boxpoints: {
valType: 'enumerated',
values: ['all', 'outliers', 'suspectedoutliers', false],
Expand Down
7 changes: 7 additions & 0 deletions src/traces/box/calc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/traces/box/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
coerce('whiskerwidth');
coerce('boxmean');

coerce('notched');
coerce('notchwidth');

Choose a reason for hiding this comment

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

We should only coerce notchwidth if notched is true.


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

Expand Down
24 changes: 20 additions & 4 deletions src/traces/box/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ? trace.notchwidth : 1;

// to support for one-sided box
var bdPos0;
Expand All @@ -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
Expand All @@ -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 === true)) ? 'H' + ln + 'L' + m + ',' + posm1 + 'L' + un + ',' + pos1 : '') + // top notched edge

Choose a reason for hiding this comment

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

That's a lot of (())... I think it can be just (notched ? ... : '')

'H' + q3 + // end of the top edge
'V' + pos0 + // right edge
(((notched === true)) ? '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 === true)) ? '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 === true)) ? '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));
Expand Down
Binary file added test/image/baselines/box_horz_notched.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/image/baselines/box_notched.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
287 changes: 287 additions & 0 deletions test/image/mocks/box_horz_notched.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
{
"data":[

Choose a reason for hiding this comment

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

Very nice test images! re: the labels getting cut off - that'll get fixed by plotly#2243 but here we might as well just give it a larger left margin so they fit.

{
"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.8",
"notched":true,
"orientation": "h",
"notchwidth":0.8
},
{
"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 = 1",
"notched":true,
"orientation": "h",
"notchwidth":1
},
{
"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,
"autosize":true
}
}
Loading