Skip to content

Commit 13daed2

Browse files
authored
Merge pull request #3234 from plotly/joyplots2
Joyplots2
2 parents cc45972 + 2517ed0 commit 13daed2

38 files changed

+455
-117
lines changed

src/traces/box/attributes.js

+14-1
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,20 @@ module.exports = {
171171
'the vertical (horizontal).'
172172
].join(' ')
173173
},
174+
175+
width: {
176+
valType: 'number',
177+
min: 0,
178+
role: 'info',
179+
dflt: 0,
180+
editType: 'calc',
181+
description: [
182+
'Sets the width of the box in data coordinate',
183+
'If *0* (default value) the width is automatically selected based on the positions',
184+
'of other box traces in the same subplot.'
185+
].join(' ')
186+
},
187+
174188
marker: {
175189
outliercolor: {
176190
valType: 'color',
@@ -244,7 +258,6 @@ module.exports = {
244258
marker: scatterAttrs.unselected.marker,
245259
editType: 'style'
246260
},
247-
248261
hoveron: {
249262
valType: 'flaglist',
250263
flags: ['boxes', 'points'],

src/traces/box/calc.js

+10-5
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,21 @@ module.exports = function calc(gd, trace) {
6464
}
6565
}
6666

67+
var cdi;
68+
var ptFilterFn = (trace.boxpoints || trace.points) === 'all' ?
69+
Lib.identity :
70+
function(pt) { return (pt.v < cdi.lf || pt.v > cdi.uf); };
71+
6772
// build calcdata trace items, one item per distinct position
6873
for(i = 0; i < pLen; i++) {
6974
if(ptsPerBin[i].length > 0) {
7075
var pts = ptsPerBin[i].sort(sortByVal);
7176
var boxVals = pts.map(extractVal);
7277
var bvLen = boxVals.length;
7378

74-
var cdi = {
75-
pos: posDistinct[i],
76-
pts: pts
77-
};
79+
cdi = {};
80+
cdi.pos = posDistinct[i];
81+
cdi.pts = pts;
7882

7983
cdi.min = boxVals[0];
8084
cdi.max = boxVals[bvLen - 1];
@@ -110,13 +114,14 @@ module.exports = function calc(gd, trace) {
110114
cdi.lo = 4 * cdi.q1 - 3 * cdi.q3;
111115
cdi.uo = 4 * cdi.q3 - 3 * cdi.q1;
112116

113-
114117
// lower and upper notches ~95% Confidence Intervals for median
115118
var iqr = cdi.q3 - cdi.q1;
116119
var mci = 1.57 * iqr / Math.sqrt(bvLen);
117120
cdi.ln = cdi.med - mci;
118121
cdi.un = cdi.med + mci;
119122

123+
cdi.pts2 = pts.filter(ptFilterFn);
124+
120125
cd.push(cdi);
121126
}
122127
}

src/traces/box/cross_trace_calc.js

+133-34
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ function crossTraceCalc(gd, plotinfo) {
2222
var orientation = orientations[i];
2323
var posAxis = orientation === 'h' ? ya : xa;
2424
var boxList = [];
25-
var minPad = 0;
26-
var maxPad = 0;
2725

2826
// make list of boxes / candlesticks
2927
// For backward compatibility, candlesticks are treated as if they *are* box traces here
@@ -40,72 +38,173 @@ function crossTraceCalc(gd, plotinfo) {
4038
trace.yaxis === ya._id
4139
) {
4240
boxList.push(j);
43-
44-
if(trace.boxpoints) {
45-
minPad = Math.max(minPad, trace.jitter - trace.pointpos - 1);
46-
maxPad = Math.max(maxPad, trace.jitter + trace.pointpos - 1);
47-
}
4841
}
4942
}
5043

51-
setPositionOffset('box', gd, boxList, posAxis, [minPad, maxPad]);
44+
setPositionOffset('box', gd, boxList, posAxis);
5245
}
5346
}
5447

55-
function setPositionOffset(traceType, gd, boxList, posAxis, pad) {
48+
function setPositionOffset(traceType, gd, boxList, posAxis) {
5649
var calcdata = gd.calcdata;
5750
var fullLayout = gd._fullLayout;
58-
var pointList = [];
51+
var axId = posAxis._id;
52+
var axLetter = axId.charAt(0);
5953

6054
// N.B. reused in violin
6155
var numKey = traceType === 'violin' ? '_numViolins' : '_numBoxes';
6256

6357
var i, j, calcTrace;
58+
var pointList = [];
59+
var shownPts = 0;
6460

6561
// make list of box points
6662
for(i = 0; i < boxList.length; i++) {
6763
calcTrace = calcdata[boxList[i]];
6864
for(j = 0; j < calcTrace.length; j++) {
6965
pointList.push(calcTrace[j].pos);
66+
shownPts += (calcTrace[j].pts2 || []).length;
7067
}
7168
}
7269

7370
if(!pointList.length) return;
7471

7572
// box plots - update dPos based on multiple traces
76-
// and then use for posAxis autorange
7773
var boxdv = Lib.distinctVals(pointList);
78-
var dPos = boxdv.minDiff / 2;
79-
80-
// if there's no duplication of x points,
81-
// disable 'group' mode by setting counter to 1
82-
if(pointList.length === boxdv.vals.length) {
83-
fullLayout[numKey] = 1;
84-
}
74+
var dPos0 = boxdv.minDiff / 2;
8575

8676
// check for forced minimum dtick
8777
Axes.minDtick(posAxis, boxdv.minDiff, boxdv.vals[0], true);
8878

89-
var gap = fullLayout[traceType + 'gap'];
90-
var groupgap = fullLayout[traceType + 'groupgap'];
91-
var padfactor = (1 - gap) * (1 - groupgap) * dPos / fullLayout[numKey];
92-
93-
// autoscale the x axis - including space for points if they're off the side
94-
// TODO: this will overdo it if the outermost boxes don't have
95-
// their points as far out as the other boxes
96-
var extremes = Axes.findExtremes(posAxis, boxdv.vals, {
97-
vpadminus: dPos + pad[0] * padfactor,
98-
vpadplus: dPos + pad[1] * padfactor
99-
});
79+
var num = fullLayout[numKey];
80+
var group = (fullLayout[traceType + 'mode'] === 'group' && num > 1);
81+
var groupFraction = 1 - fullLayout[traceType + 'gap'];
82+
var groupGapFraction = 1 - fullLayout[traceType + 'groupgap'];
10083

10184
for(i = 0; i < boxList.length; i++) {
10285
calcTrace = calcdata[boxList[i]];
103-
// set the width of all boxes
104-
calcTrace[0].t.dPos = dPos;
105-
// link extremes to all boxes
106-
calcTrace[0].trace._extremes[posAxis._id] = extremes;
107-
}
10886

87+
var trace = calcTrace[0].trace;
88+
var t = calcTrace[0].t;
89+
var width = trace.width;
90+
var side = trace.side;
91+
92+
// position coordinate delta
93+
var dPos;
94+
// box half width;
95+
var bdPos;
96+
// box center offset
97+
var bPos;
98+
// half-width within which to accept hover for this box/violin
99+
// always split the distance to the closest box/violin
100+
var wHover;
101+
102+
if(width) {
103+
dPos = bdPos = wHover = width / 2;
104+
bPos = 0;
105+
} else {
106+
dPos = dPos0;
107+
bdPos = dPos * groupFraction * groupGapFraction / (group ? num : 1);
108+
bPos = group ? 2 * dPos * (-0.5 + (t.num + 0.5) / num) * groupFraction : 0;
109+
wHover = dPos * (group ? groupFraction / num : 1);
110+
}
111+
t.dPos = dPos;
112+
t.bPos = bPos;
113+
t.bdPos = bdPos;
114+
t.wHover = wHover;
115+
116+
// box/violin-only value-space push value
117+
var pushplus;
118+
var pushminus;
119+
// edge of box/violin
120+
var edge = bPos + bdPos;
121+
var edgeplus;
122+
var edgeminus;
123+
124+
if(side === 'positive') {
125+
pushplus = dPos * (width ? 1 : 0.5);
126+
edgeplus = edge;
127+
pushminus = edgeplus = bPos;
128+
} else if(side === 'negative') {
129+
pushplus = edgeplus = bPos;
130+
pushminus = dPos * (width ? 1 : 0.5);
131+
edgeminus = edge;
132+
} else {
133+
pushplus = pushminus = dPos;
134+
edgeplus = edgeminus = edge;
135+
}
136+
137+
// value-space padding
138+
var vpadplus;
139+
var vpadminus;
140+
// pixel-space padding
141+
var ppadplus;
142+
var ppadminus;
143+
// do we add 5% of both sides (for points beyond box/violin)
144+
var padded = false;
145+
// does this trace show points?
146+
var hasPts = (trace.boxpoints || trace.points) && (shownPts > 0);
147+
148+
if(hasPts) {
149+
var pointpos = trace.pointpos;
150+
var jitter = trace.jitter;
151+
var ms = trace.marker.size / 2;
152+
153+
var pp = 0;
154+
if((pointpos + jitter) >= 0) {
155+
pp = edge * (pointpos + jitter);
156+
if(pp > pushplus) {
157+
// (++) beyond plus-value, use pp
158+
padded = true;
159+
ppadplus = ms;
160+
vpadplus = pp;
161+
} else if(pp > edgeplus) {
162+
// (+), use push-value (it's bigger), but add px-pad
163+
ppadplus = ms;
164+
vpadplus = pushplus;
165+
}
166+
}
167+
if(pp <= pushplus) {
168+
// (->) fallback to push value
169+
vpadplus = pushplus;
170+
}
171+
172+
var pm = 0;
173+
if((pointpos - jitter) <= 0) {
174+
pm = -edge * (pointpos - jitter);
175+
if(pm > pushminus) {
176+
// (--) beyond plus-value, use pp
177+
padded = true;
178+
ppadminus = ms;
179+
vpadminus = pm;
180+
} else if(pm > edgeminus) {
181+
// (-), use push-value (it's bigger), but add px-pad
182+
ppadminus = ms;
183+
vpadminus = pushminus;
184+
}
185+
}
186+
if(pm <= pushminus) {
187+
// (<-) fallback to push value
188+
vpadminus = pushminus;
189+
}
190+
} else {
191+
vpadplus = pushplus;
192+
vpadminus = pushminus;
193+
}
194+
195+
// calcdata[i][j] are in ascending order
196+
var firstPos = calcTrace[0].pos;
197+
var lastPos = calcTrace[calcTrace.length - 1].pos;
198+
199+
trace._extremes[axId] = Axes.findExtremes(posAxis, [firstPos, lastPos], {
200+
padded: padded,
201+
vpadminus: vpadminus,
202+
vpadplus: vpadplus,
203+
// N.B. SVG px-space positive/negative
204+
ppadminus: {x: ppadminus, y: ppadplus}[axLetter],
205+
ppadplus: {x: ppadplus, y: ppadminus}[axLetter],
206+
});
207+
}
109208
}
110209

111210
module.exports = {

src/traces/box/defaults.js

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
2828

2929
coerce('whiskerwidth');
3030
coerce('boxmean');
31+
coerce('width');
3132

3233
var notched = coerce('notched', traceIn.notchwidth !== undefined);
3334
if(notched) coerce('notchwidth');

src/traces/box/layout_attributes.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ module.exports = {
2222
'If *group*, the boxes are plotted next to one another',
2323
'centered around the shared location.',
2424
'If *overlay*, the boxes are plotted over one another,',
25-
'you might need to set *opacity* to see them multiple boxes.'
25+
'you might need to set *opacity* to see them multiple boxes.',
26+
'Has no effect on traces that have *width* set.'
2627
].join(' ')
2728
},
2829
boxgap: {
@@ -34,7 +35,8 @@ module.exports = {
3435
editType: 'calc',
3536
description: [
3637
'Sets the gap (in plot fraction) between boxes of',
37-
'adjacent location coordinates.'
38+
'adjacent location coordinates.',
39+
'Has no effect on traces that have *width* set.'
3840
].join(' ')
3941
},
4042
boxgroupgap: {
@@ -46,7 +48,8 @@ module.exports = {
4648
editType: 'calc',
4749
description: [
4850
'Sets the gap (in plot fraction) between boxes of',
49-
'the same location coordinate.'
51+
'the same location coordinate.',
52+
'Has no effect on traces that have *width* set.'
5053
].join(' ')
5154
}
5255
};

src/traces/box/plot.js

+3-21
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,18 @@ var JITTERCOUNT = 5; // points either side of this to include
1818
var JITTERSPREAD = 0.01; // fraction of IQR to count as "dense"
1919

2020
function plot(gd, plotinfo, cdbox, boxLayer) {
21-
var fullLayout = gd._fullLayout;
2221
var xa = plotinfo.xaxis;
2322
var ya = plotinfo.yaxis;
24-
var numBoxes = fullLayout._numBoxes;
25-
var groupFraction = (1 - fullLayout.boxgap);
26-
var group = (fullLayout.boxmode === 'group' && numBoxes > 1);
2723

2824
Lib.makeTraceGroups(boxLayer, cdbox, 'trace boxes').each(function(cd) {
2925
var plotGroup = d3.select(this);
3026
var cd0 = cd[0];
3127
var t = cd0.t;
3228
var trace = cd0.trace;
3329
if(!plotinfo.isRangePlot) cd0.node3 = plotGroup;
34-
// box half width
35-
var bdPos = t.dPos * groupFraction * (1 - fullLayout.boxgroupgap) / (group ? numBoxes : 1);
36-
// box center offset
37-
var bPos = group ? 2 * t.dPos * (-0.5 + (t.num + 0.5) / numBoxes) * groupFraction : 0;
30+
3831
// whisker width
39-
var wdPos = bdPos * trace.whiskerwidth;
32+
t.wdPos = t.bdPos * trace.whiskerwidth;
4033

4134
if(trace.visible !== true || t.empty) {
4235
plotGroup.remove();
@@ -53,14 +46,6 @@ function plot(gd, plotinfo, cdbox, boxLayer) {
5346
valAxis = ya;
5447
}
5548

56-
// save the box size and box position for use by hover
57-
t.bPos = bPos;
58-
t.bdPos = bdPos;
59-
t.wdPos = wdPos;
60-
// half-width within which to accept hover for this box
61-
// always split the distance to the closest box
62-
t.wHover = t.dPos * (group ? groupFraction / numBoxes : 1);
63-
6449
plotBoxAndWhiskers(plotGroup, {pos: posAxis, val: valAxis}, trace, t);
6550
plotPoints(plotGroup, {x: xa, y: ya}, trace, t);
6651
plotBoxMean(plotGroup, {pos: posAxis, val: valAxis}, trace, t);
@@ -192,10 +177,7 @@ function plotPoints(sel, axes, trace, t) {
192177
var paths = gPoints.selectAll('path')
193178
.data(function(d) {
194179
var i;
195-
196-
var pts = mode === 'all' ?
197-
d.pts :
198-
d.pts.filter(function(pt) { return (pt.v < d.lf || pt.v > d.uf); });
180+
var pts = d.pts2;
199181

200182
// normally use IQR, but if this is 0 or too small, use max-min
201183
var typicalSpread = Math.max((d.max - d.min) / 10, d.q3 - d.q1);

0 commit comments

Comments
 (0)