Skip to content

Commit 3438eae

Browse files
committed
first cut violin
1 parent c8f38ff commit 3438eae

15 files changed

+455
-14
lines changed

lib/index-cartesian.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ Plotly.register([
1919
require('./histogram2dcontour'),
2020
require('./pie'),
2121
require('./contour'),
22-
require('./scatterternary')
22+
require('./scatterternary'),
23+
require('./violin')
2324
]);
2425

2526
module.exports = Plotly;

lib/index.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Plotly.register([
2121
require('./pie'),
2222
require('./contour'),
2323
require('./scatterternary'),
24-
require('./sankey'),
24+
require('./violin'),
2525

2626
require('./scatter3d'),
2727
require('./surface'),
@@ -34,10 +34,13 @@ Plotly.register([
3434
require('./pointcloud'),
3535
require('./heatmapgl'),
3636
require('./parcoords'),
37-
require('./table'),
3837

3938
require('./scattermapbox'),
4039

40+
require('./sankey'),
41+
42+
require('./table'),
43+
4144
require('./carpet'),
4245
require('./scattercarpet'),
4346
require('./contourcarpet'),

lib/violin.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Copyright 2012-2017, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
module.exports = require('../src/traces/violin');

src/plots/cartesian/constants.js

+2-7
Original file line numberDiff line numberDiff line change
@@ -57,18 +57,13 @@ module.exports = {
5757
DFLTRANGEX: [-1, 6],
5858
DFLTRANGEY: [-1, 4],
5959

60-
// Layers to keep trace types in the right order.
61-
// from back to front:
62-
// 1. heatmaps, 2D histos and contour maps
63-
// 2. bars / 1D histos
64-
// 3. errorbars for bars and scatter
65-
// 4. scatter
66-
// 5. box plots
60+
// Layers to keep trace types in the right order
6761
traceLayerClasses: [
6862
'imagelayer',
6963
'maplayer',
7064
'barlayer',
7165
'carpetlayer',
66+
'violinlayer',
7267
'boxlayer',
7368
'scatterlayer'
7469
],

src/plots/plots.js

+1
Original file line numberDiff line numberDiff line change
@@ -2160,6 +2160,7 @@ plots.doCalcdata = function(gd, traces) {
21602160

21612161
// how many box/violins plots do we have (in case they're grouped)
21622162
fullLayout._numBoxes = 0;
2163+
fullLayout._numViolins = 0;
21632164

21642165
// for calculating avg luminosity of heatmaps
21652166
gd._hmpixcount = 0;

src/traces/box/plot.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,8 @@ function plotPoints(sel, plotinfo, trace, t) {
161161
var bdPos = t.bdPos;
162162
var bPos = t.bPos;
163163

164-
var mode = trace.boxpoints;
164+
// TODO ... unfortunately
165+
var mode = trace.boxpoints || trace.points;
165166

166167
// repeatable pseudorandom number generator
167168
seed();

src/traces/box/set_positions.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@ var Registry = require('../../registry');
1212
var Axes = require('../../plots/cartesian/axes');
1313
var Lib = require('../../lib');
1414

15-
1615
module.exports = function setPositions(gd, plotinfo) {
1716
var fullLayout = gd._fullLayout;
1817
var xa = plotinfo.xaxis;
1918
var ya = plotinfo.yaxis;
2019
var orientations = ['v', 'h'];
2120

21+
// TODO figure this out
22+
// should violins and boxes share 'num' fields?
2223
var numKey = '_numBoxes';
2324

2425
var posAxis, i, j, k;
@@ -84,8 +85,10 @@ module.exports = function setPositions(gd, plotinfo) {
8485
gd.calcdata[boxListIndex][0].t.dPos = dPos;
8586
}
8687

87-
var gap = fullLayout.boxgap;
88-
var groupgap = fullLayout.boxgroupgap;
88+
// TODO this won't work when both boxes and violins are present
89+
// on same graph
90+
var gap = fullLayout.boxgap || fullLayout.violingap;
91+
var groupgap = fullLayout.boxgroupgap || fullLayout.violingroupgap;
8992

9093
// autoscale the x axis - including space for points if they're off the side
9194
// TODO: this will overdo it if the outermost boxes don't have

src/traces/violin/attributes.js

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* Copyright 2012-2017, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
var boxAttrs = require('../box/attributes');
12+
var scatterAttrs = require('../scatter/attributes');
13+
var extendFlat = require('../../lib/extend').extendFlat;
14+
15+
module.exports = {
16+
y: boxAttrs.y,
17+
x: boxAttrs.x,
18+
x0: boxAttrs.x0,
19+
y0: boxAttrs.y0,
20+
name: boxAttrs.name,
21+
orientation: extendFlat({}, boxAttrs.orientation, {
22+
description: [
23+
'Sets the orientation of the violin(s).',
24+
'If *v* (*h*), the distribution is visualized along',
25+
'the vertical (horizontal).'
26+
].join(' ')
27+
}),
28+
29+
bandwidth: {
30+
valType: 'number',
31+
min: 0,
32+
role: 'info',
33+
editType: 'plot',
34+
description: [
35+
'Sets the bandwidth used to compute the kernel density estimate.',
36+
'By default, the bandwidth is determined by Silverman\'s rule of thumb.'
37+
].join(' ')
38+
},
39+
scaleby: {
40+
valType: 'enumerated',
41+
values: ['width', 'area', 'count'],
42+
dflt: 'width',
43+
role: 'info',
44+
editType: 'calc',
45+
description: [
46+
'Sets the method by which the width of each violin is determined.',
47+
'*width* means each violin has the same (max) width',
48+
'*area* means each violin has the same area',
49+
'*count* means the violins are scaled by the number of sample points making',
50+
'up each violin.'
51+
].join('')
52+
},
53+
span: {
54+
valType: 'info_array',
55+
items: [
56+
{valType: 'any', editType: 'plot'},
57+
{valType: 'any', editType: 'plot'}
58+
],
59+
role: 'info',
60+
editType: 'plot',
61+
description: [
62+
'Sets the span in data space for which the density function will be computed.',
63+
'By default, the span goes from the minimum value to maximum value in the sample.'
64+
].join(' ')
65+
},
66+
side: {
67+
valType: 'enumerated',
68+
values: ['both', 'left', 'right'],
69+
dflt: 'both',
70+
role: 'info',
71+
editType: 'plot',
72+
description: [
73+
'Determines which side of the position line the density function making up',
74+
'one half of a is plotting.',
75+
'Useful when comparing two violin traces under *overlay* mode, where one trace.'
76+
].join(' ')
77+
},
78+
79+
// TODO update description
80+
points: boxAttrs.boxpoints,
81+
jitter: boxAttrs.jitter,
82+
pointpos: boxAttrs.pointpos,
83+
marker: boxAttrs.marker,
84+
text: boxAttrs.text,
85+
86+
// TODO need attribute(s) similar to 'boxmean' to toggle lines for:
87+
// - mean
88+
// - median
89+
// - std
90+
// - quartiles
91+
92+
line: {
93+
color: {
94+
valType: 'color',
95+
role: 'style',
96+
editType: 'style',
97+
description: 'Sets the color of line bounding the violin(s).'
98+
},
99+
width: {
100+
valType: 'number',
101+
role: 'style',
102+
min: 0,
103+
dflt: 2,
104+
editType: 'style',
105+
description: 'Sets the width (in px) of line bounding the violin(s).'
106+
},
107+
smoothing: scatterAttrs.line.smoothing,
108+
editType: 'plot'
109+
},
110+
111+
fillcolor: boxAttrs.fillcolor,
112+
hoveron: boxAttrs.hoveron
113+
};

src/traces/violin/calc.js

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* Copyright 2012-2017, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
var Lib = require('../../lib');
12+
var boxCalc = require('../box/calc');
13+
14+
var kernels = {
15+
gaussian: function(v) {
16+
return (1 / Math.sqrt(2 * Math.PI)) * Math.exp(-0.5 * v * v);
17+
}
18+
};
19+
20+
module.exports = function calc(gd, trace) {
21+
var cd = boxCalc(gd, trace);
22+
23+
if(cd[0].t.empty) return cd;
24+
25+
for(var i = 0; i < cd.length; i++) {
26+
var cdi = cd[i];
27+
var vals = cdi.pts.map(extractVal);
28+
var len = vals.length;
29+
var span = trace.span || [cdi.min, cdi.max];
30+
var dist = span[1] - span[0];
31+
// sample standard deviation
32+
var ssd = Lib.stdev(vals, len - 1, cdi.mean);
33+
var bandwidthDflt = ruleOfThumbBandwidth(vals, ssd, cdi.q3 - cdi.q1);
34+
var bandwidth = trace.bandwidth || bandwidthDflt;
35+
var kde = makeKDE(vals, kernels.gaussian, bandwidth);
36+
// step that well covers the bandwidth and is multiple of span distance
37+
var n = Math.ceil(dist / (Math.min(bandwidthDflt, bandwidth) / 3));
38+
var step = dist / n;
39+
40+
cdi.density = new Array(n);
41+
cdi.violinMaxWidth = 0;
42+
43+
for(var k = 0, t = span[0]; t < (span[1] + step / 2); k++, t += step) {
44+
var v = kde(t);
45+
cdi.violinMaxWidth = Math.max(cdi.violinMaxWidth, v);
46+
cdi.density[k] = {v: v, t: t};
47+
}
48+
}
49+
50+
return cd;
51+
};
52+
53+
// Default to Silveman's rule of thumb:
54+
// - https://en.wikipedia.org/wiki/Kernel_density_estimation#A_rule-of-thumb_bandwidth_estimator
55+
// - https://github.com/statsmodels/statsmodels/blob/master/statsmodels/nonparametric/bandwidths.py
56+
function ruleOfThumbBandwidth(vals, ssd, iqr) {
57+
var a = Math.min(ssd, iqr / 1.349);
58+
return 1.059 * a * Math.pow(vals.length, -0.2);
59+
}
60+
61+
function makeKDE(vals, kernel, bandwidth) {
62+
var len = vals.length;
63+
var factor = 1 / (len * bandwidth);
64+
65+
// don't use Lib.aggNums to skip isNumeric checks
66+
return function(x) {
67+
var sum = 0;
68+
for(var i = 0; i < len; i++) {
69+
sum += kernel((x - vals[i]) / bandwidth);
70+
}
71+
return factor * sum;
72+
};
73+
}
74+
75+
function extractVal(o) { return o.v; }

src/traces/violin/defaults.js

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Copyright 2012-2017, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
var Lib = require('../../lib');
12+
var Color = require('../../components/color');
13+
14+
var boxDefaults = require('../box/defaults');
15+
var attributes = require('./attributes');
16+
17+
module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
18+
function coerce(attr, dflt) {
19+
return Lib.coerce(traceIn, traceOut, attributes, attr, dflt);
20+
}
21+
22+
boxDefaults.handleSampleDefaults(traceIn, traceOut, coerce, layout);
23+
if(traceOut.visible === false) return;
24+
25+
coerce('bandwidth');
26+
coerce('scaleby');
27+
coerce('span');
28+
coerce('side');
29+
30+
coerce('line.color', (traceIn.marker || {}).color || defaultColor);
31+
coerce('line.width');
32+
coerce('line.smoothing');
33+
coerce('fillcolor', Color.addOpacity(traceOut.line.color, 0.5));
34+
35+
boxDefaults.handlePointsDefaults(traceIn, traceOut, coerce, {prefix: ''});
36+
};

src/traces/violin/index.js

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* Copyright 2012-2017, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
module.exports = {
12+
attributes: require('./attributes'),
13+
layoutAttributes: require('./layout_attributes'),
14+
supplyDefaults: require('./defaults'),
15+
supplyLayoutDefaults: require('./layout_defaults'),
16+
calc: require('./calc'),
17+
setPositions: require('../box/set_positions'),
18+
plot: require('./plot'),
19+
style: require('./style'),
20+
hoverPoints: require('../box/hover'),
21+
selectPoints: require('../box/select'),
22+
23+
moduleType: 'trace',
24+
name: 'violin',
25+
basePlotModule: require('../../plots/cartesian'),
26+
// TODO
27+
// - should maybe rename 'box' category to something more general
28+
categories: ['cartesian', 'symbols', 'oriented', 'box', 'showLegend'],
29+
meta: {
30+
description: [
31+
'In vertical (horizontal) violin plots,',
32+
'statistics are computed using `y` (`x`) values.',
33+
'By supplying an `x` (`y`) array, one violin per distinct x (y) value',
34+
'is drawn',
35+
'If no `x` (`y`) {array} is provided, a single violin is drawn.',
36+
'That violin position is then positioned with',
37+
'with `name` or with `x0` (`y0`) if provided.'
38+
].join(' ')
39+
}
40+
};

0 commit comments

Comments
 (0)