Skip to content

Commit 81ddaca

Browse files
authored
Merge pull request #2116 from plotly/violins-dev
Violin plots
2 parents 4b8b10d + 789121c commit 81ddaca

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+3389
-433
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/components/fx/hover.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ function _hover(gd, evt, subplot, noHoverEvent) {
381381

382382
// Now find the points.
383383
if(trace._module && trace._module.hoverPoints) {
384-
var newPoints = trace._module.hoverPoints(pointData, xval, yval, mode);
384+
var newPoints = trace._module.hoverPoints(pointData, xval, yval, mode, fullLayout._hoverlayer);
385385
if(newPoints) {
386386
var newPoint;
387387
for(var newPointNum = 0; newPointNum < newPoints.length; newPointNum++) {

src/components/legend/style.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ module.exports = function style(s, gd) {
197197
var trace = d[0].trace,
198198
pts = d3.select(this).select('g.legendpoints')
199199
.selectAll('path.legendbox')
200-
.data(Registry.traceIs(trace, 'box') && trace.visible ? [d] : []);
200+
.data(Registry.traceIs(trace, 'box-violin') && trace.visible ? [d] : []);
201201
pts.enter().append('path').classed('legendbox', true)
202202
// if we want the median bar, prepend M6,0H-6
203203
.attr('d', 'M6,6H-6V-6H6Z')

src/components/modebar/manage.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,8 @@ function isSelectable(fullData) {
179179
if(scatterSubTypes.hasMarkers(trace) || scatterSubTypes.hasText(trace)) {
180180
selectable = true;
181181
}
182-
} else if(Registry.traceIs(trace, 'box')) {
183-
if(trace.boxpoints === 'all') {
182+
} else if(Registry.traceIs(trace, 'box-violin')) {
183+
if(trace.boxpoints === 'all' || trace.points === 'all') {
184184
selectable = true;
185185
}
186186
}

src/lib/geometry2d.js

+49
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,52 @@ exports.getVisibleSegment = function getVisibleSegment(path, bounds, buffer) {
193193
Math.abs(pt0.y - ptTotal.y) < 0.1
194194
};
195195
};
196+
197+
/**
198+
* Find point on SVG path corresponding to a given constraint coordinate
199+
*
200+
* @param {SVGPathElement} path
201+
* @param {Number} val : constraint coordinate value
202+
* @param {String} coord : 'x' or 'y' the constraint coordinate
203+
* @param {Object} opts :
204+
* - {Number} pathLength : supply total path length before hand
205+
* - {Number} tolerance
206+
* - {Number} iterationLimit
207+
* @return {SVGPoint}
208+
*/
209+
exports.findPointOnPath = function findPointOnPath(path, val, coord, opts) {
210+
opts = opts || {};
211+
212+
var pathLength = opts.pathLength || path.getTotalLength();
213+
var tolerance = opts.tolerance || 1e-3;
214+
var iterationLimit = opts.iterationLimit || 30;
215+
216+
// if path starts at a val greater than the path tail (like on vertical violins),
217+
// we must flip the sign of the computed diff.
218+
var mul = path.getPointAtLength(0)[coord] > path.getPointAtLength(pathLength)[coord] ? -1 : 1;
219+
220+
var i = 0;
221+
var b0 = 0;
222+
var b1 = pathLength;
223+
var mid;
224+
var pt;
225+
var diff;
226+
227+
while(i < iterationLimit) {
228+
mid = (b0 + b1) / 2;
229+
pt = path.getPointAtLength(mid);
230+
diff = pt[coord] - val;
231+
232+
if(Math.abs(diff) < tolerance) {
233+
return pt;
234+
} else {
235+
if(mul * diff > 0) {
236+
b1 = mid;
237+
} else {
238+
b0 = mid;
239+
}
240+
i++;
241+
}
242+
}
243+
return pt;
244+
};

src/lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ lib.segmentDistance = geom2dModule.segmentDistance;
8282
lib.getTextLocation = geom2dModule.getTextLocation;
8383
lib.clearLocationCache = geom2dModule.clearLocationCache;
8484
lib.getVisibleSegment = geom2dModule.getVisibleSegment;
85+
lib.findPointOnPath = geom2dModule.findPointOnPath;
8586

8687
var extendModule = require('./extend');
8788
lib.extendFlat = extendModule.extendFlat;

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/cartesian/type_defaults.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ function setAutoType(ax, data) {
7575

7676
for(var i = 0; i < data.length; i++) {
7777
trace = data[i];
78-
if(!Registry.traceIs(trace, 'box') ||
78+
if(!Registry.traceIs(trace, 'box-violin') ||
7979
(trace[axLetter + 'axis'] || axLetter) !== id) continue;
8080

8181
if(trace[posLetter] !== undefined) boxPositions.push(trace[posLetter][0]);
@@ -113,7 +113,7 @@ function getBoxPosLetter(trace) {
113113

114114
function isBoxWithoutPositionCoords(trace, axLetter) {
115115
var posLetter = getBoxPosLetter(trace),
116-
isBox = Registry.traceIs(trace, 'box'),
116+
isBox = Registry.traceIs(trace, 'box-violin'),
117117
isCandlestick = Registry.traceIs(trace._fullInput || {}, 'candlestick');
118118

119119
return (

src/plots/plots.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -1342,7 +1342,6 @@ plots.purge = function(gd) {
13421342
delete gd.firstscatter;
13431343
delete gd._hmlumcount;
13441344
delete gd._hmpixcount;
1345-
delete gd.numboxes;
13461345
delete gd._transitionData;
13471346
delete gd._transitioning;
13481347
delete gd._initialAutoSize;
@@ -2159,8 +2158,12 @@ plots.doCalcdata = function(gd, traces) {
21592158
// firstscatter: fill-to-next on the first trace goes to zero
21602159
gd.firstscatter = true;
21612160

2162-
// how many box plots do we have (in case they're grouped)
2163-
gd.numboxes = 0;
2161+
// how many box/violins plots do we have (in case they're grouped)
2162+
fullLayout._numBoxes = 0;
2163+
fullLayout._numViolins = 0;
2164+
2165+
// initialize violin per-scale-group stats container
2166+
fullLayout._violinScaleGroupStats = {};
21642167

21652168
// for calculating avg luminosity of heatmaps
21662169
gd._hmpixcount = 0;

src/traces/box/calc.js

+15-9
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,19 @@ var Axes = require('../../plots/cartesian/axes');
1515

1616
// outlier definition based on http://www.physics.csbsju.edu/stats/box2.html
1717
module.exports = function calc(gd, trace) {
18+
var fullLayout = gd._fullLayout;
1819
var xa = Axes.getFromId(gd, trace.xaxis || 'x');
1920
var ya = Axes.getFromId(gd, trace.yaxis || 'y');
20-
var orientation = trace.orientation;
2121
var cd = [];
2222

23+
// N.B. violin reuses same Box.calc
24+
var numKey = trace.type === 'violin' ? '_numViolins' : '_numBoxes';
25+
2326
var i;
2427
var valAxis, valLetter;
2528
var posAxis, posLetter;
2629

27-
if(orientation === 'h') {
30+
if(trace.orientation === 'h') {
2831
valAxis = xa;
2932
valLetter = 'x';
3033
posAxis = ya;
@@ -37,7 +40,7 @@ module.exports = function calc(gd, trace) {
3740
}
3841

3942
var val = valAxis.makeCalcdata(trace, valLetter);
40-
var pos = getPos(trace, posLetter, posAxis, val, gd.numboxes);
43+
var pos = getPos(trace, posLetter, posAxis, val, fullLayout[numKey]);
4144

4245
var dv = Lib.distinctVals(pos);
4346
var posDistinct = dv.vals;
@@ -115,13 +118,16 @@ module.exports = function calc(gd, trace) {
115118

116119
if(cd.length > 0) {
117120
cd[0].t = {
118-
boxnum: gd.numboxes,
119-
dPos: dPos
121+
num: fullLayout[numKey],
122+
dPos: dPos,
123+
posLetter: posLetter,
124+
valLetter: valLetter
120125
};
121-
gd.numboxes++;
126+
127+
fullLayout[numKey]++;
122128
return cd;
123129
} else {
124-
return [{t: {emptybox: true}}];
130+
return [{t: {empty: true}}];
125131
}
126132
};
127133

@@ -130,7 +136,7 @@ module.exports = function calc(gd, trace) {
130136
// so if you want one box
131137
// per trace, set x0 (y0) to the x (y) value or category for this trace
132138
// (or set x (y) to a constant array matching y (x))
133-
function getPos(trace, posLetter, posAxis, val, numboxes) {
139+
function getPos(trace, posLetter, posAxis, val, num) {
134140
if(posLetter in trace) {
135141
return posAxis.makeCalcdata(trace, posLetter);
136142
}
@@ -150,7 +156,7 @@ function getPos(trace, posLetter, posAxis, val, numboxes) {
150156
)) {
151157
pos0 = trace.name;
152158
} else {
153-
pos0 = numboxes;
159+
pos0 = num;
154160
}
155161

156162
var pos0c = posAxis.d2c(pos0, 0, trace[posLetter + 'calendar']);

src/traces/box/defaults.js

+30-13
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,25 @@ var Color = require('../../components/color');
1414

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

17-
module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
17+
function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
1818
function coerce(attr, dflt) {
1919
return Lib.coerce(traceIn, traceOut, attributes, attr, dflt);
2020
}
2121

22+
handleSampleDefaults(traceIn, traceOut, coerce, layout);
23+
if(traceOut.visible === false) return;
24+
25+
coerce('line.color', (traceIn.marker || {}).color || defaultColor);
26+
coerce('line.width');
27+
coerce('fillcolor', Color.addOpacity(traceOut.line.color, 0.5));
28+
29+
coerce('whiskerwidth');
30+
coerce('boxmean');
31+
32+
handlePointsDefaults(traceIn, traceOut, coerce, {prefix: 'box'});
33+
}
34+
35+
function handleSampleDefaults(traceIn, traceOut, coerce, layout) {
2236
var y = coerce('y');
2337
var x = coerce('x');
2438

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

4155
coerce('orientation', defaultOrientation);
56+
}
4257

43-
coerce('line.color', (traceIn.marker || {}).color || defaultColor);
44-
coerce('line.width');
45-
coerce('fillcolor', Color.addOpacity(traceOut.line.color, 0.5));
46-
47-
coerce('whiskerwidth');
48-
coerce('boxmean');
58+
function handlePointsDefaults(traceIn, traceOut, coerce, opts) {
59+
var prefix = opts.prefix;
4960

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

53-
var boxpoints = coerce(
54-
'boxpoints',
64+
var points = coerce(
65+
prefix + 'points',
5566
(outlierColorDflt || lineoutliercolor) ? 'suspectedoutliers' : undefined
5667
);
5768

58-
if(boxpoints) {
59-
coerce('jitter', boxpoints === 'all' ? 0.3 : 0);
60-
coerce('pointpos', boxpoints === 'all' ? -1.5 : 0);
69+
if(points) {
70+
coerce('jitter', points === 'all' ? 0.3 : 0);
71+
coerce('pointpos', points === 'all' ? -1.5 : 0);
6172

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

69-
if(boxpoints === 'suspectedoutliers') {
80+
if(points === 'suspectedoutliers') {
7081
coerce('marker.line.outliercolor', traceOut.marker.color);
7182
coerce('marker.line.outlierwidth');
7283
}
@@ -77,4 +88,10 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
7788
}
7889

7990
coerce('hoveron');
91+
}
92+
93+
module.exports = {
94+
supplyDefaults: supplyDefaults,
95+
handleSampleDefaults: handleSampleDefaults,
96+
handlePointsDefaults: handlePointsDefaults
8097
};

0 commit comments

Comments
 (0)