Skip to content

Commit a706f2a

Browse files
committed
factor out box/whiskers and mean/sd plotting routine
- to make them reusable in violin/plot.js - add support for asymmetric bdPos to support one-sided violins
1 parent ea43b25 commit a706f2a

File tree

1 file changed

+123
-71
lines changed

1 file changed

+123
-71
lines changed

src/traces/box/plot.js

Lines changed: 123 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ function plot(gd, plotinfo, cdbox) {
6464
return;
6565
}
6666

67-
// set axis via orientation
6867
var posAxis, valAxis;
68+
6969
if(trace.orientation === 'h') {
7070
posAxis = ya;
7171
valAxis = xa;
@@ -77,91 +77,90 @@ function plot(gd, plotinfo, cdbox) {
7777
// save the box size and box position for use by hover
7878
t.bPos = bPos;
7979
t.bdPos = bdPos;
80+
t.wdPos = wdPos;
8081

8182
// boxes and whiskers
82-
sel.selectAll('path.box')
83-
.data(Lib.identity)
84-
.enter().append('path')
85-
.style('vector-effect', 'non-scaling-stroke')
86-
.attr('class', 'box')
87-
.each(function(d) {
88-
var posc = posAxis.c2p(d.pos + bPos, true),
89-
pos0 = posAxis.c2p(d.pos + bPos - bdPos, true),
90-
pos1 = posAxis.c2p(d.pos + bPos + bdPos, true),
91-
posw0 = posAxis.c2p(d.pos + bPos - wdPos, true),
92-
posw1 = posAxis.c2p(d.pos + bPos + wdPos, true),
93-
q1 = valAxis.c2p(d.q1, true),
94-
q3 = valAxis.c2p(d.q3, true),
95-
// make sure median isn't identical to either of the
96-
// quartiles, so we can see it
97-
m = Lib.constrain(valAxis.c2p(d.med, true),
98-
Math.min(q1, q3) + 1, Math.max(q1, q3) - 1),
99-
lf = valAxis.c2p(trace.boxpoints === false ? d.min : d.lf, true),
100-
uf = valAxis.c2p(trace.boxpoints === false ? d.max : d.uf, true);
101-
102-
if(trace.orientation === 'h') {
103-
d3.select(this).attr('d',
104-
'M' + m + ',' + pos0 + 'V' + pos1 + // median line
105-
'M' + q1 + ',' + pos0 + 'V' + pos1 + 'H' + q3 + 'V' + pos0 + 'Z' + // box
106-
'M' + q1 + ',' + posc + 'H' + lf + 'M' + q3 + ',' + posc + 'H' + uf + // whiskers
107-
((trace.whiskerwidth === 0) ? '' : // whisker caps
108-
'M' + lf + ',' + posw0 + 'V' + posw1 + 'M' + uf + ',' + posw0 + 'V' + posw1));
109-
} else {
110-
d3.select(this).attr('d',
111-
'M' + pos0 + ',' + m + 'H' + pos1 + // median line
112-
'M' + pos0 + ',' + q1 + 'H' + pos1 + 'V' + q3 + 'H' + pos0 + 'Z' + // box
113-
'M' + posc + ',' + q1 + 'V' + lf + 'M' + posc + ',' + q3 + 'V' + uf + // whiskers
114-
((trace.whiskerwidth === 0) ? '' : // whisker caps
115-
'M' + posw0 + ',' + lf + 'H' + posw1 + 'M' + posw0 + ',' + uf + 'H' + posw1));
116-
}
117-
});
83+
plotBoxAndWhiskers(sel, {pos: posAxis, val: valAxis}, trace, t);
11884

11985
// draw points, if desired
12086
if(trace.boxpoints) {
121-
plotPoints(sel, plotinfo, trace, t);
87+
plotPoints(sel, {x: xa, y: ya}, trace, t);
12288
}
12389

12490
// draw mean (and stdev diamond) if desired
12591
if(trace.boxmean) {
126-
sel.selectAll('path.mean')
127-
.data(Lib.identity)
128-
.enter().append('path')
129-
.attr('class', 'mean')
130-
.style({
131-
fill: 'none',
132-
'vector-effect': 'non-scaling-stroke'
133-
})
134-
.each(function(d) {
135-
var posc = posAxis.c2p(d.pos + bPos, true),
136-
pos0 = posAxis.c2p(d.pos + bPos - bdPos, true),
137-
pos1 = posAxis.c2p(d.pos + bPos + bdPos, true),
138-
m = valAxis.c2p(d.mean, true),
139-
sl = valAxis.c2p(d.mean - d.sd, true),
140-
sh = valAxis.c2p(d.mean + d.sd, true);
141-
if(trace.orientation === 'h') {
142-
d3.select(this).attr('d',
143-
'M' + m + ',' + pos0 + 'V' + pos1 +
144-
((trace.boxmean !== 'sd') ? '' :
145-
'm0,0L' + sl + ',' + posc + 'L' + m + ',' + pos0 + 'L' + sh + ',' + posc + 'Z'));
146-
}
147-
else {
148-
d3.select(this).attr('d',
149-
'M' + pos0 + ',' + m + 'H' + pos1 +
150-
((trace.boxmean !== 'sd') ? '' :
151-
'm0,0L' + posc + ',' + sl + 'L' + pos0 + ',' + m + 'L' + posc + ',' + sh + 'Z'));
152-
}
153-
});
92+
plotBoxMean(sel, {pos: posAxis, val: valAxis}, trace, t);
15493
}
15594
});
15695
}
15796

158-
function plotPoints(sel, plotinfo, trace, t) {
159-
var xa = plotinfo.xaxis;
160-
var ya = plotinfo.yaxis;
97+
function plotBoxAndWhiskers(sel, axes, trace, t) {
98+
var posAxis = axes.pos;
99+
var valAxis = axes.val;
100+
var bPos = t.bPos;
101+
var wdPos = t.wdPos || 0;
102+
var bPosPxOffset = t.bPosPxOffset || 0;
103+
var whiskerWidth = trace.whiskerwidth || 0;
104+
105+
// to support for one-sided box
106+
var bdPos0;
107+
var bdPos1;
108+
if(Array.isArray(t.bdPos)) {
109+
bdPos0 = t.bdPos[0];
110+
bdPos1 = t.bdPos[1];
111+
} else {
112+
bdPos0 = t.bdPos;
113+
bdPos1 = t.bdPos;
114+
}
115+
116+
sel.selectAll('path.box')
117+
.data(Lib.identity)
118+
.enter().append('path')
119+
.style('vector-effect', 'non-scaling-stroke')
120+
.attr('class', 'box')
121+
.each(function(d) {
122+
var pos = d.pos;
123+
var posc = posAxis.c2p(pos + bPos, true) + bPosPxOffset;
124+
var pos0 = posAxis.c2p(pos + bPos - bdPos0, true) + bPosPxOffset;
125+
var pos1 = posAxis.c2p(pos + bPos + bdPos1, true) + bPosPxOffset;
126+
var posw0 = posAxis.c2p(pos + bPos - wdPos, true) + bPosPxOffset;
127+
var posw1 = posAxis.c2p(pos + bPos + wdPos, true) + bPosPxOffset;
128+
var q1 = valAxis.c2p(d.q1, true);
129+
var q3 = valAxis.c2p(d.q3, true);
130+
// make sure median isn't identical to either of the
131+
// quartiles, so we can see it
132+
var m = Lib.constrain(
133+
valAxis.c2p(d.med, true),
134+
Math.min(q1, q3) + 1, Math.max(q1, q3) - 1
135+
);
136+
var lf = valAxis.c2p(trace.boxpoints === false ? d.min : d.lf, true);
137+
var uf = valAxis.c2p(trace.boxpoints === false ? d.max : d.uf, true);
138+
139+
if(trace.orientation === 'h') {
140+
d3.select(this).attr('d',
141+
'M' + m + ',' + pos0 + 'V' + pos1 + // median line
142+
'M' + q1 + ',' + pos0 + 'V' + pos1 + 'H' + q3 + 'V' + pos0 + 'Z' + // box
143+
'M' + q1 + ',' + posc + 'H' + lf + 'M' + q3 + ',' + posc + 'H' + uf + // whiskers
144+
((whiskerWidth === 0) ? '' : // whisker caps
145+
'M' + lf + ',' + posw0 + 'V' + posw1 + 'M' + uf + ',' + posw0 + 'V' + posw1));
146+
} else {
147+
d3.select(this).attr('d',
148+
'M' + pos0 + ',' + m + 'H' + pos1 + // median line
149+
'M' + pos0 + ',' + q1 + 'H' + pos1 + 'V' + q3 + 'H' + pos0 + 'Z' + // box
150+
'M' + posc + ',' + q1 + 'V' + lf + 'M' + posc + ',' + q3 + 'V' + uf + // whiskers
151+
((whiskerWidth === 0) ? '' : // whisker caps
152+
'M' + posw0 + ',' + lf + 'H' + posw1 + 'M' + posw0 + ',' + uf + 'H' + posw1));
153+
}
154+
});
155+
}
156+
157+
function plotPoints(sel, axes, trace, t) {
158+
var xa = axes.x;
159+
var ya = axes.y;
161160
var bdPos = t.bdPos;
162161
var bPos = t.bPos;
163162

164-
// TODO ... unfortunately
163+
// to support violin innerbox
165164
var mode = trace.boxpoints || trace.points;
166165

167166
// repeatable pseudorandom number generator
@@ -258,7 +257,60 @@ function plotPoints(sel, plotinfo, trace, t) {
258257
.call(Drawing.translatePoints, xa, ya);
259258
}
260259

260+
function plotBoxMean(sel, axes, trace, t) {
261+
var posAxis = axes.pos;
262+
var valAxis = axes.val;
263+
var bPos = t.bPos;
264+
var bPosPxOffset = t.bPosPxOffset || 0;
265+
266+
// to support for one-sided box
267+
var bdPos0;
268+
var bdPos1;
269+
if(Array.isArray(t.bdPos)) {
270+
bdPos0 = t.bdPos[0];
271+
bdPos1 = t.bdPos[1];
272+
} else {
273+
bdPos0 = t.bdPos;
274+
bdPos1 = t.bdPos;
275+
}
276+
277+
sel.selectAll('path.mean')
278+
.data(Lib.identity)
279+
.enter().append('path')
280+
.attr('class', 'mean')
281+
.style({
282+
fill: 'none',
283+
'vector-effect': 'non-scaling-stroke'
284+
})
285+
.each(function(d) {
286+
var posc = posAxis.c2p(d.pos + bPos, true) + bPosPxOffset;
287+
var pos0 = posAxis.c2p(d.pos + bPos - bdPos0, true) + bPosPxOffset;
288+
var pos1 = posAxis.c2p(d.pos + bPos + bdPos1, true) + bPosPxOffset;
289+
var m = valAxis.c2p(d.mean, true);
290+
var sl = valAxis.c2p(d.mean - d.sd, true);
291+
var sh = valAxis.c2p(d.mean + d.sd, true);
292+
293+
if(trace.orientation === 'h') {
294+
d3.select(this).attr('d',
295+
'M' + m + ',' + pos0 + 'V' + pos1 +
296+
(trace.boxmean === 'sd' ?
297+
'm0,0L' + sl + ',' + posc + 'L' + m + ',' + pos0 + 'L' + sh + ',' + posc + 'Z' :
298+
'')
299+
);
300+
} else {
301+
d3.select(this).attr('d',
302+
'M' + pos0 + ',' + m + 'H' + pos1 +
303+
(trace.boxmean === 'sd' ?
304+
'm0,0L' + posc + ',' + sl + 'L' + pos0 + ',' + m + 'L' + posc + ',' + sh + 'Z' :
305+
'')
306+
);
307+
}
308+
});
309+
}
310+
261311
module.exports = {
262312
plot: plot,
263-
plotPoints: plotPoints
313+
plotBoxAndWhiskers: plotBoxAndWhiskers,
314+
plotPoints: plotPoints,
315+
plotBoxMean: plotBoxMean
264316
};

0 commit comments

Comments
 (0)