Skip to content

Violin plots #2116

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

Merged
merged 26 commits into from
Nov 1, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c578cde
replace gd.numboxes by fullLayout._numBoxes
etpinard Oct 24, 2017
8f34227
replace 'emptybox' with 'empty' in box calc item
etpinard Oct 24, 2017
c8f38ff
factor out box defaults and boxpoint plot methods
etpinard Oct 24, 2017
3438eae
first cut violin
etpinard Oct 24, 2017
d779509
first cut violin mocks
etpinard Oct 24, 2017
bb252a5
Merge branch 'master' into violins-dev
etpinard Oct 30, 2017
ea43b25
rename 'box' category 'box-violin'
etpinard Oct 31, 2017
a706f2a
factor out box/whiskers and mean/sd plotting routine
etpinard Oct 31, 2017
1eb453b
split box hover into onBoxes and onPoints routines
etpinard Oct 31, 2017
7769f20
implement violinmode, violingroup and violingroupgap
etpinard Oct 31, 2017
ad51966
2nd cut violin calc/plot attributes + improve violin curve paths
etpinard Oct 31, 2017
4a40fc7
add findPointOnPath geometry2d util function
etpinard Oct 31, 2017
6ffc379
implement violin 'inner' style options
etpinard Oct 31, 2017
dfa918f
pass hoverlayer to trace module hoverPoints
etpinard Oct 31, 2017
bc6bc02
implement violin hover
etpinard Oct 31, 2017
e737664
2nd cut violin mocks
etpinard Oct 31, 2017
e625c44
1st cut violin jasmine tests
etpinard Oct 31, 2017
14bded3
fill violin attribute descriptions + fix typo in comment
etpinard Oct 31, 2017
48758d8
fixup scalemode 'count' calculation
etpinard Oct 31, 2017
17f65d0
add inner box and mean line to side-by-side violin mock
etpinard Oct 31, 2017
71700de
2nd cut violin jasmine tests
etpinard Oct 31, 2017
677aacc
remove 'kernel' from violin attributes
etpinard Nov 1, 2017
0a32b98
update box and meanline attribute syntax
etpinard Nov 1, 2017
3c9e0a0
add violin style mock
etpinard Nov 1, 2017
ea66dea
add editType to new violin attr containers
etpinard Nov 1, 2017
789121c
update violin layout attr descriptions
etpinard Nov 1, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 30 additions & 13 deletions src/traces/box/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,25 @@ var Color = require('../../components/color');

var attributes = require('./attributes');

module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
function coerce(attr, dflt) {
return Lib.coerce(traceIn, traceOut, attributes, attr, dflt);
}

handleSampleDefaults(traceIn, traceOut, coerce, layout);
if(traceOut.visible === false) return;

coerce('line.color', (traceIn.marker || {}).color || defaultColor);
coerce('line.width');
coerce('fillcolor', Color.addOpacity(traceOut.line.color, 0.5));

coerce('whiskerwidth');
coerce('boxmean');

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

function handleSampleDefaults(traceIn, traceOut, coerce, layout) {
var y = coerce('y');
var x = coerce('x');

Expand All @@ -39,25 +53,22 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
handleCalendarDefaults(traceIn, traceOut, ['x', 'y'], layout);

coerce('orientation', defaultOrientation);
}

coerce('line.color', (traceIn.marker || {}).color || defaultColor);
coerce('line.width');
coerce('fillcolor', Color.addOpacity(traceOut.line.color, 0.5));

coerce('whiskerwidth');
coerce('boxmean');
function handlePointsDefaults(traceIn, traceOut, coerce, opts) {
var prefix = opts.prefix;

var outlierColorDflt = Lib.coerce2(traceIn, traceOut, attributes, 'marker.outliercolor');
var lineoutliercolor = coerce('marker.line.outliercolor');

var boxpoints = coerce(
'boxpoints',
var points = coerce(
prefix + 'points',
(outlierColorDflt || lineoutliercolor) ? 'suspectedoutliers' : undefined
);

if(boxpoints) {
coerce('jitter', boxpoints === 'all' ? 0.3 : 0);
coerce('pointpos', boxpoints === 'all' ? -1.5 : 0);
if(points) {
coerce('jitter', points === 'all' ? 0.3 : 0);
coerce('pointpos', points === 'all' ? -1.5 : 0);

coerce('marker.symbol');
coerce('marker.opacity');
Expand All @@ -66,7 +77,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
coerce('marker.line.color');
coerce('marker.line.width');

if(boxpoints === 'suspectedoutliers') {
if(points === 'suspectedoutliers') {
coerce('marker.line.outliercolor', traceOut.marker.color);
coerce('marker.line.outlierwidth');
}
Expand All @@ -77,4 +88,10 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
}

coerce('hoveron');
}

module.exports = {
supplyDefaults: supplyDefaults,
handleSampleDefaults: handleSampleDefaults,
handlePointsDefaults: handlePointsDefaults
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I made a separate reorganisation commit, to show a new way to factor out common trace module blocks. I think this method is a little more consistent with ES6 modules. For example here, supplyDefaults would be the default export.

};
6 changes: 3 additions & 3 deletions src/traces/box/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ var Box = {};

Box.attributes = require('./attributes');
Box.layoutAttributes = require('./layout_attributes');
Box.supplyDefaults = require('./defaults');
Box.supplyLayoutDefaults = require('./layout_defaults');
Box.supplyDefaults = require('./defaults').supplyDefaults;
Box.supplyLayoutDefaults = require('./layout_defaults').supplyLayoutDefaults;
Box.calc = require('./calc');
Box.setPositions = require('./set_positions');
Box.plot = require('./plot');
Box.plot = require('./plot').plot;
Box.style = require('./style');
Box.hoverPoints = require('./hover');
Box.selectPoints = require('./select');
Expand Down
24 changes: 16 additions & 8 deletions src/traces/box/layout_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@ var Registry = require('../../registry');
var Lib = require('../../lib');
var layoutAttributes = require('./layout_attributes');

module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
function coerce(attr, dflt) {
return Lib.coerce(layoutIn, layoutOut, layoutAttributes, attr, dflt);
}

function _supply(layoutIn, layoutOut, fullData, coerce, prefix) {
var hasBoxes;
for(var i = 0; i < fullData.length; i++) {
if(Registry.traceIs(fullData[i], 'box')) {
Expand All @@ -26,7 +22,19 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
}
if(!hasBoxes) return;

coerce('boxmode');
coerce('boxgap');
coerce('boxgroupgap');
coerce(prefix + 'mode');
coerce(prefix + 'gap');
coerce(prefix + 'groupgap');
}

function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
function coerce(attr, dflt) {
return Lib.coerce(layoutIn, layoutOut, layoutAttributes, attr, dflt);
}
_supply(layoutIn, layoutOut, fullData, coerce, 'box');
}

module.exports = {
supplyLayoutDefaults: supplyLayoutDefaults,
_supply: _supply
};
202 changes: 109 additions & 93 deletions src/traces/box/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ function rand() {
var JITTERCOUNT = 5; // points either side of this to include
var JITTERSPREAD = 0.01; // fraction of IQR to count as "dense"

module.exports = function plot(gd, plotinfo, cdbox) {
function plot(gd, plotinfo, cdbox) {
var fullLayout = gd._fullLayout;
var xa = plotinfo.xaxis;
var ya = plotinfo.yaxis;
Expand Down Expand Up @@ -78,9 +78,6 @@ module.exports = function plot(gd, plotinfo, cdbox) {
t.bPos = bPos;
t.bdPos = bdPos;

// repeatable pseudorandom number generator
seed();

// boxes and whiskers
sel.selectAll('path.box')
.data(Lib.identity)
Expand Down Expand Up @@ -121,95 +118,7 @@ module.exports = function plot(gd, plotinfo, cdbox) {

// draw points, if desired
if(trace.boxpoints) {
sel.selectAll('g.points')
// since box plot points get an extra level of nesting, each
// box needs the trace styling info
.data(function(d) {
d.forEach(function(v) {
v.t = t;
v.trace = trace;
});
return d;
})
.enter().append('g')
.attr('class', 'points')
.selectAll('path')
.data(function(d) {
var i;

var pts = trace.boxpoints === 'all' ?
d.pts :
d.pts.filter(function(pt) { return (pt.v < d.lf || pt.v > d.uf); });

// normally use IQR, but if this is 0 or too small, use max-min
var typicalSpread = Math.max((d.max - d.min) / 10, d.q3 - d.q1);
var minSpread = typicalSpread * 1e-9;
var spreadLimit = typicalSpread * JITTERSPREAD;
var jitterFactors = [];
var maxJitterFactor = 0;
var newJitter;

// dynamic jitter
if(trace.jitter) {
if(typicalSpread === 0) {
// edge case of no spread at all: fall back to max jitter
maxJitterFactor = 1;
jitterFactors = new Array(pts.length);
for(i = 0; i < pts.length; i++) {
jitterFactors[i] = 1;
}
} else {
for(i = 0; i < pts.length; i++) {
var i0 = Math.max(0, i - JITTERCOUNT);
var pmin = pts[i0].v;
var i1 = Math.min(pts.length - 1, i + JITTERCOUNT);
var pmax = pts[i1].v;

if(trace.boxpoints !== 'all') {
if(pts[i].v < d.lf) pmax = Math.min(pmax, d.lf);
else pmin = Math.max(pmin, d.uf);
}

var jitterFactor = Math.sqrt(spreadLimit * (i1 - i0) / (pmax - pmin + minSpread)) || 0;
jitterFactor = Lib.constrain(Math.abs(jitterFactor), 0, 1);

jitterFactors.push(jitterFactor);
maxJitterFactor = Math.max(jitterFactor, maxJitterFactor);
}
}
newJitter = trace.jitter * 2 / maxJitterFactor;
}

// fills in 'x' and 'y' in calcdata 'pts' item
for(i = 0; i < pts.length; i++) {
var pt = pts[i];
var v = pt.v;

var jitterOffset = trace.jitter ?
(newJitter * jitterFactors[i] * (rand() - 0.5)) :
0;

var posPx = d.pos + bPos + bdPos * (trace.pointpos + jitterOffset);

if(trace.orientation === 'h') {
pt.y = posPx;
pt.x = v;
} else {
pt.x = posPx;
pt.y = v;
}

// tag suspected outliers
if(trace.boxpoints === 'suspectedoutliers' && v < d.uo && v > d.lo) {
pt.so = true;
}
}

return pts;
})
.enter().append('path')
.classed('point', true)
.call(Drawing.translatePoints, xa, ya);
plotPoints(sel, plotinfo, trace, t);
}

// draw mean (and stdev diamond) if desired
Expand Down Expand Up @@ -244,4 +153,111 @@ module.exports = function plot(gd, plotinfo, cdbox) {
});
}
});
}

function plotPoints(sel, plotinfo, trace, t) {
var xa = plotinfo.xaxis;
var ya = plotinfo.yaxis;
var bdPos = t.bdPos;
var bPos = t.bPos;

var mode = trace.boxpoints;

// repeatable pseudorandom number generator
seed();

sel.selectAll('g.points')
// since box plot points get an extra level of nesting, each
// box needs the trace styling info
.data(function(d) {
d.forEach(function(v) {
v.t = t;
v.trace = trace;
});
return d;
})
.enter().append('g')
.attr('class', 'points')
.selectAll('path')
.data(function(d) {
var i;

var pts = mode === 'all' ?
d.pts :
d.pts.filter(function(pt) { return (pt.v < d.lf || pt.v > d.uf); });

// normally use IQR, but if this is 0 or too small, use max-min
var typicalSpread = Math.max((d.max - d.min) / 10, d.q3 - d.q1);
var minSpread = typicalSpread * 1e-9;
var spreadLimit = typicalSpread * JITTERSPREAD;
var jitterFactors = [];
var maxJitterFactor = 0;
var newJitter;

// dynamic jitter
if(trace.jitter) {
if(typicalSpread === 0) {
// edge case of no spread at all: fall back to max jitter
maxJitterFactor = 1;
jitterFactors = new Array(pts.length);
for(i = 0; i < pts.length; i++) {
jitterFactors[i] = 1;
}
} else {
for(i = 0; i < pts.length; i++) {
var i0 = Math.max(0, i - JITTERCOUNT);
var pmin = pts[i0].v;
var i1 = Math.min(pts.length - 1, i + JITTERCOUNT);
var pmax = pts[i1].v;

if(mode !== 'all') {
if(pts[i].v < d.lf) pmax = Math.min(pmax, d.lf);
else pmin = Math.max(pmin, d.uf);
}

var jitterFactor = Math.sqrt(spreadLimit * (i1 - i0) / (pmax - pmin + minSpread)) || 0;
jitterFactor = Lib.constrain(Math.abs(jitterFactor), 0, 1);

jitterFactors.push(jitterFactor);
maxJitterFactor = Math.max(jitterFactor, maxJitterFactor);
}
}
newJitter = trace.jitter * 2 / maxJitterFactor;
}

// fills in 'x' and 'y' in calcdata 'pts' item
for(i = 0; i < pts.length; i++) {
var pt = pts[i];
var v = pt.v;

var jitterOffset = trace.jitter ?
(newJitter * jitterFactors[i] * (rand() - 0.5)) :
0;

var posPx = d.pos + bPos + bdPos * (trace.pointpos + jitterOffset);

if(trace.orientation === 'h') {
pt.y = posPx;
pt.x = v;
} else {
pt.x = posPx;
pt.y = v;
}

// tag suspected outliers
if(mode === 'suspectedoutliers' && v < d.uo && v > d.lo) {
pt.so = true;
}
}

return pts;
})
.enter().append('path')
.classed('point', true)
.call(Drawing.translatePoints, xa, ya);
}

module.exports = {
plot: plot,
plotPoints: plotPoints
};