Skip to content

Commit 7e1c98c

Browse files
authored
Merge pull request #2785 from plotly/box-violin-innerpart-removal
Fix box & violin inner parts removal
2 parents c4453da + 1e4900c commit 7e1c98c

File tree

5 files changed

+203
-92
lines changed

5 files changed

+203
-92
lines changed

src/traces/box/plot.js

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -73,18 +73,9 @@ function plot(gd, plotinfo, cdbox, boxLayer) {
7373
// always split the distance to the closest box
7474
t.wHover = t.dPos * (group ? groupFraction / numBoxes : 1);
7575

76-
// boxes and whiskers
7776
plotBoxAndWhiskers(sel, {pos: posAxis, val: valAxis}, trace, t);
78-
79-
// draw points, if desired
80-
if(trace.boxpoints) {
81-
plotPoints(sel, {x: xa, y: ya}, trace, t);
82-
}
83-
84-
// draw mean (and stdev diamond) if desired
85-
if(trace.boxmean) {
86-
plotBoxMean(sel, {pos: posAxis, val: valAxis}, trace, t);
87-
}
77+
plotPoints(sel, {x: xa, y: ya}, trace, t);
78+
plotBoxMean(sel, {pos: posAxis, val: valAxis}, trace, t);
8879
});
8980
}
9081

@@ -109,7 +100,10 @@ function plotBoxAndWhiskers(sel, axes, trace, t) {
109100
bdPos1 = t.bdPos;
110101
}
111102

112-
var paths = sel.selectAll('path.box').data(Lib.identity);
103+
var paths = sel.selectAll('path.box').data((
104+
trace.type !== 'violin' ||
105+
trace.box
106+
) ? Lib.identity : []);
113107

114108
paths.enter().append('path')
115109
.style('vector-effect', 'non-scaling-stroke')
@@ -187,16 +181,18 @@ function plotPoints(sel, axes, trace, t) {
187181
// repeatable pseudo-random number generator
188182
Lib.seedPseudoRandom();
189183

190-
var gPoints = sel.selectAll('g.points')
191-
// since box plot points get an extra level of nesting, each
192-
// box needs the trace styling info
193-
.data(function(d) {
194-
d.forEach(function(v) {
195-
v.t = t;
196-
v.trace = trace;
197-
});
198-
return d;
184+
// since box plot points get an extra level of nesting, each
185+
// box needs the trace styling info
186+
var fn = function(d) {
187+
d.forEach(function(v) {
188+
v.t = t;
189+
v.trace = trace;
199190
});
191+
return d;
192+
};
193+
194+
var gPoints = sel.selectAll('g.points')
195+
.data(mode ? fn : []);
200196

201197
gPoints.enter().append('g')
202198
.attr('class', 'points');
@@ -292,6 +288,9 @@ function plotBoxMean(sel, axes, trace, t) {
292288
var bPos = t.bPos;
293289
var bPosPxOffset = t.bPosPxOffset || 0;
294290

291+
// to support violin mean lines
292+
var mode = trace.boxmean || (trace.meanline || {}).visible;
293+
295294
// to support for one-sided box
296295
var bdPos0;
297296
var bdPos1;
@@ -303,7 +302,10 @@ function plotBoxMean(sel, axes, trace, t) {
303302
bdPos1 = t.bdPos;
304303
}
305304

306-
var paths = sel.selectAll('path.mean').data(Lib.identity);
305+
var paths = sel.selectAll('path.mean').data((
306+
(trace.type === 'box' && trace.boxmean) ||
307+
(trace.type === 'violin' && trace.box && trace.meanline)
308+
) ? Lib.identity : []);
307309

308310
paths.enter().append('path')
309311
.attr('class', 'mean')
@@ -325,14 +327,14 @@ function plotBoxMean(sel, axes, trace, t) {
325327
if(trace.orientation === 'h') {
326328
d3.select(this).attr('d',
327329
'M' + m + ',' + pos0 + 'V' + pos1 +
328-
(trace.boxmean === 'sd' ?
330+
(mode === 'sd' ?
329331
'm0,0L' + sl + ',' + posc + 'L' + m + ',' + pos0 + 'L' + sh + ',' + posc + 'Z' :
330332
'')
331333
);
332334
} else {
333335
d3.select(this).attr('d',
334336
'M' + pos0 + ',' + m + 'H' + pos1 +
335-
(trace.boxmean === 'sd' ?
337+
(mode === 'sd' ?
336338
'm0,0L' + posc + ',' + sl + 'L' + pos0 + ',' + m + 'L' + posc + ',' + sh + 'Z' :
337339
'')
338340
);

src/traces/violin/plot.js

Lines changed: 52 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,6 @@ module.exports = function plot(gd, plotinfo, cd, violinLayer) {
6969
var hasBothSides = trace.side === 'both';
7070
var hasPositiveSide = hasBothSides || trace.side === 'positive';
7171
var hasNegativeSide = hasBothSides || trace.side === 'negative';
72-
var hasBox = trace.box && trace.box.visible;
73-
var hasMeanLine = trace.meanline && trace.meanline.visible;
7472
var groupStats = fullLayout._violinScaleGroupStats[trace.scalegroup];
7573

7674
var violins = sel.selectAll('path.violin').data(Lib.identity);
@@ -149,66 +147,61 @@ module.exports = function plot(gd, plotinfo, cd, violinLayer) {
149147
d.pathLength = d.path.getTotalLength() / (hasBothSides ? 2 : 1);
150148
});
151149

152-
if(hasBox) {
153-
var boxWidth = trace.box.width;
154-
var boxLineWidth = trace.box.line.width;
155-
var bdPosScaled;
156-
var bPosPxOffset;
150+
var boxAttrs = trace.box || {};
151+
var boxWidth = boxAttrs.width;
152+
var boxLineWidth = (boxAttrs.line || {}).width;
153+
var bdPosScaled;
154+
var bPosPxOffset;
155+
156+
if(hasBothSides) {
157+
bdPosScaled = bdPos * boxWidth;
158+
bPosPxOffset = 0;
159+
} else if(hasPositiveSide) {
160+
bdPosScaled = [0, bdPos * boxWidth / 2];
161+
bPosPxOffset = -boxLineWidth;
162+
} else {
163+
bdPosScaled = [bdPos * boxWidth / 2, 0];
164+
bPosPxOffset = boxLineWidth;
165+
}
157166

158-
if(hasBothSides) {
159-
bdPosScaled = bdPos * boxWidth;
160-
bPosPxOffset = 0;
161-
} else if(hasPositiveSide) {
162-
bdPosScaled = [0, bdPos * boxWidth / 2];
163-
bPosPxOffset = -boxLineWidth;
164-
} else {
165-
bdPosScaled = [bdPos * boxWidth / 2, 0];
166-
bPosPxOffset = boxLineWidth;
167-
}
167+
// inner box
168+
boxPlot.plotBoxAndWhiskers(sel, {pos: posAxis, val: valAxis}, trace, {
169+
bPos: bPos,
170+
bdPos: bdPosScaled,
171+
bPosPxOffset: bPosPxOffset
172+
});
168173

169-
boxPlot.plotBoxAndWhiskers(sel, {pos: posAxis, val: valAxis}, trace, {
170-
bPos: bPos,
171-
bdPos: bdPosScaled,
172-
bPosPxOffset: bPosPxOffset
173-
});
174-
175-
// if both box and meanline are visible, show mean line inside box
176-
if(hasMeanLine) {
177-
boxPlot.plotBoxMean(sel, {pos: posAxis, val: valAxis}, trace, {
178-
bPos: bPos,
179-
bdPos: bdPosScaled,
180-
bPosPxOffset: bPosPxOffset
181-
});
182-
}
183-
}
184-
else {
185-
if(hasMeanLine) {
186-
var meanPaths = sel.selectAll('path.mean').data(Lib.identity);
187-
188-
meanPaths.enter().append('path')
189-
.attr('class', 'mean')
190-
.style({
191-
fill: 'none',
192-
'vector-effect': 'non-scaling-stroke'
193-
});
194-
195-
meanPaths.exit().remove();
196-
197-
meanPaths.each(function(d) {
198-
var v = valAxis.c2p(d.mean, true);
199-
var p = helpers.getPositionOnKdePath(d, trace, v);
200-
201-
d3.select(this).attr('d',
202-
trace.orientation === 'h' ?
203-
'M' + v + ',' + p[0] + 'V' + p[1] :
204-
'M' + p[0] + ',' + v + 'H' + p[1]
205-
);
206-
});
207-
}
208-
}
174+
// meanline insider box
175+
boxPlot.plotBoxMean(sel, {pos: posAxis, val: valAxis}, trace, {
176+
bPos: bPos,
177+
bdPos: bdPosScaled,
178+
bPosPxOffset: bPosPxOffset
179+
});
209180

210-
if(trace.points) {
211-
boxPlot.plotPoints(sel, {x: xa, y: ya}, trace, t);
181+
var fn;
182+
if(!(trace.box || {}).visible && (trace.meanline || {}).visible) {
183+
fn = Lib.identity;
212184
}
185+
186+
// N.B. use different class name than boxPlot.plotBoxMean,
187+
// to avoid selectAll conflict
188+
var meanPaths = sel.selectAll('path.meanline').data(fn || []);
189+
meanPaths.enter().append('path')
190+
.attr('class', 'meanline')
191+
.style('fill', 'none')
192+
.style('vector-effect', 'non-scaling-stroke');
193+
meanPaths.exit().remove();
194+
meanPaths.each(function(d) {
195+
var v = valAxis.c2p(d.mean, true);
196+
var p = helpers.getPositionOnKdePath(d, trace, v);
197+
198+
d3.select(this).attr('d',
199+
trace.orientation === 'h' ?
200+
'M' + v + ',' + p[0] + 'V' + p[1] :
201+
'M' + p[0] + ',' + v + 'H' + p[1]
202+
);
203+
});
204+
205+
boxPlot.plotPoints(sel, {x: xa, y: ya}, trace, t);
213206
});
214207
};

src/traces/violin/style.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,17 @@ module.exports = function style(gd, cd) {
3535
.call(Color.stroke, boxLine.color)
3636
.call(Color.fill, box.fillcolor);
3737

38+
var meanLineStyle = {
39+
'stroke-width': meanLineWidth + 'px',
40+
'stroke-dasharray': (2 * meanLineWidth) + 'px,' + meanLineWidth + 'px'
41+
};
42+
3843
sel.selectAll('path.mean')
39-
.style({
40-
'stroke-width': meanLineWidth + 'px',
41-
'stroke-dasharray': (2 * meanLineWidth) + 'px,' + meanLineWidth + 'px'
42-
})
44+
.style(meanLineStyle)
45+
.call(Color.stroke, meanline.color);
46+
47+
sel.selectAll('path.meanline')
48+
.style(meanLineStyle)
4349
.call(Color.stroke, meanline.color);
4450

4551
stylePoints(sel, trace, gd);

test/jasmine/tests/box_test.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ var Lib = require('@src/lib');
33

44
var Box = require('@src/traces/box');
55

6+
var d3 = require('d3');
67
var createGraphDiv = require('../assets/create_graph_div');
78
var destroyGraphDiv = require('../assets/destroy_graph_div');
89
var failTest = require('../assets/fail_test');
@@ -348,3 +349,54 @@ describe('Box edge cases', function() {
348349
.then(done);
349350
});
350351
});
352+
353+
describe('Test box restyle:', function() {
354+
var gd;
355+
356+
beforeEach(function() {
357+
gd = createGraphDiv();
358+
});
359+
360+
afterEach(destroyGraphDiv);
361+
362+
it('should be able to add/remove innner parts', function(done) {
363+
var fig = Lib.extendDeep({}, require('@mocks/box_plot_jitter.json'));
364+
// start with just 1 box
365+
delete fig.data[0].boxpoints;
366+
367+
function _assertOne(msg, exp, trace3, k, query) {
368+
expect(trace3.selectAll(query).size())
369+
.toBe(exp[k] || 0, k + ' - ' + msg);
370+
}
371+
372+
function _assert(msg, exp) {
373+
var trace3 = d3.select(gd).select('.boxlayer > .trace');
374+
_assertOne(msg, exp, trace3, 'boxCnt', 'path.box');
375+
_assertOne(msg, exp, trace3, 'meanlineCnt', 'path.mean');
376+
_assertOne(msg, exp, trace3, 'ptsCnt', 'path.point');
377+
}
378+
379+
Plotly.plot(gd, fig)
380+
.then(function() {
381+
_assert('base', {boxCnt: 1});
382+
})
383+
.then(function() { return Plotly.restyle(gd, 'boxmean', true); })
384+
.then(function() {
385+
_assert('with meanline', {boxCnt: 1, meanlineCnt: 1});
386+
})
387+
.then(function() { return Plotly.restyle(gd, 'boxmean', 'sd'); })
388+
.then(function() {
389+
_assert('with mean+sd line', {boxCnt: 1, meanlineCnt: 1});
390+
})
391+
.then(function() { return Plotly.restyle(gd, 'boxpoints', 'all'); })
392+
.then(function() {
393+
_assert('with mean+sd line + pts', {boxCnt: 1, meanlineCnt: 1, ptsCnt: 9});
394+
})
395+
.then(function() { return Plotly.restyle(gd, 'boxmean', false); })
396+
.then(function() {
397+
_assert('with pts', {boxCnt: 1, ptsCnt: 9});
398+
})
399+
.catch(failTest)
400+
.then(done);
401+
});
402+
});

0 commit comments

Comments
 (0)