diff --git a/lib/index-cartesian.js b/lib/index-cartesian.js
index 4d07f5f5f09..5818a717748 100644
--- a/lib/index-cartesian.js
+++ b/lib/index-cartesian.js
@@ -19,7 +19,8 @@ Plotly.register([
require('./histogram2dcontour'),
require('./pie'),
require('./contour'),
- require('./scatterternary')
+ require('./scatterternary'),
+ require('./violin')
]);
module.exports = Plotly;
diff --git a/lib/index.js b/lib/index.js
index b64ed888557..7dac73a1f64 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -21,7 +21,7 @@ Plotly.register([
require('./pie'),
require('./contour'),
require('./scatterternary'),
- require('./sankey'),
+ require('./violin'),
require('./scatter3d'),
require('./surface'),
@@ -34,10 +34,13 @@ Plotly.register([
require('./pointcloud'),
require('./heatmapgl'),
require('./parcoords'),
- require('./table'),
require('./scattermapbox'),
+ require('./sankey'),
+
+ require('./table'),
+
require('./carpet'),
require('./scattercarpet'),
require('./contourcarpet'),
diff --git a/lib/violin.js b/lib/violin.js
new file mode 100644
index 00000000000..d1d0ea51c3f
--- /dev/null
+++ b/lib/violin.js
@@ -0,0 +1,11 @@
+/**
+* Copyright 2012-2017, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+'use strict';
+
+module.exports = require('../src/traces/violin');
diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js
index fc219542914..37d80fbc126 100644
--- a/src/components/fx/hover.js
+++ b/src/components/fx/hover.js
@@ -381,7 +381,7 @@ function _hover(gd, evt, subplot, noHoverEvent) {
// Now find the points.
if(trace._module && trace._module.hoverPoints) {
- var newPoints = trace._module.hoverPoints(pointData, xval, yval, mode);
+ var newPoints = trace._module.hoverPoints(pointData, xval, yval, mode, fullLayout._hoverlayer);
if(newPoints) {
var newPoint;
for(var newPointNum = 0; newPointNum < newPoints.length; newPointNum++) {
diff --git a/src/components/legend/style.js b/src/components/legend/style.js
index c6037e79d07..08199c2a888 100644
--- a/src/components/legend/style.js
+++ b/src/components/legend/style.js
@@ -197,7 +197,7 @@ module.exports = function style(s, gd) {
var trace = d[0].trace,
pts = d3.select(this).select('g.legendpoints')
.selectAll('path.legendbox')
- .data(Registry.traceIs(trace, 'box') && trace.visible ? [d] : []);
+ .data(Registry.traceIs(trace, 'box-violin') && trace.visible ? [d] : []);
pts.enter().append('path').classed('legendbox', true)
// if we want the median bar, prepend M6,0H-6
.attr('d', 'M6,6H-6V-6H6Z')
diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js
index 783d1bb7f49..bedee3dcb7d 100644
--- a/src/components/modebar/manage.js
+++ b/src/components/modebar/manage.js
@@ -179,8 +179,8 @@ function isSelectable(fullData) {
if(scatterSubTypes.hasMarkers(trace) || scatterSubTypes.hasText(trace)) {
selectable = true;
}
- } else if(Registry.traceIs(trace, 'box')) {
- if(trace.boxpoints === 'all') {
+ } else if(Registry.traceIs(trace, 'box-violin')) {
+ if(trace.boxpoints === 'all' || trace.points === 'all') {
selectable = true;
}
}
diff --git a/src/lib/geometry2d.js b/src/lib/geometry2d.js
index a946ccf5e23..8cb21eae047 100644
--- a/src/lib/geometry2d.js
+++ b/src/lib/geometry2d.js
@@ -193,3 +193,52 @@ exports.getVisibleSegment = function getVisibleSegment(path, bounds, buffer) {
Math.abs(pt0.y - ptTotal.y) < 0.1
};
};
+
+/**
+ * Find point on SVG path corresponding to a given constraint coordinate
+ *
+ * @param {SVGPathElement} path
+ * @param {Number} val : constraint coordinate value
+ * @param {String} coord : 'x' or 'y' the constraint coordinate
+ * @param {Object} opts :
+ * - {Number} pathLength : supply total path length before hand
+ * - {Number} tolerance
+ * - {Number} iterationLimit
+ * @return {SVGPoint}
+ */
+exports.findPointOnPath = function findPointOnPath(path, val, coord, opts) {
+ opts = opts || {};
+
+ var pathLength = opts.pathLength || path.getTotalLength();
+ var tolerance = opts.tolerance || 1e-3;
+ var iterationLimit = opts.iterationLimit || 30;
+
+ // if path starts at a val greater than the path tail (like on vertical violins),
+ // we must flip the sign of the computed diff.
+ var mul = path.getPointAtLength(0)[coord] > path.getPointAtLength(pathLength)[coord] ? -1 : 1;
+
+ var i = 0;
+ var b0 = 0;
+ var b1 = pathLength;
+ var mid;
+ var pt;
+ var diff;
+
+ while(i < iterationLimit) {
+ mid = (b0 + b1) / 2;
+ pt = path.getPointAtLength(mid);
+ diff = pt[coord] - val;
+
+ if(Math.abs(diff) < tolerance) {
+ return pt;
+ } else {
+ if(mul * diff > 0) {
+ b1 = mid;
+ } else {
+ b0 = mid;
+ }
+ i++;
+ }
+ }
+ return pt;
+};
diff --git a/src/lib/index.js b/src/lib/index.js
index 563541ed5fe..b34099227d6 100644
--- a/src/lib/index.js
+++ b/src/lib/index.js
@@ -82,6 +82,7 @@ lib.segmentDistance = geom2dModule.segmentDistance;
lib.getTextLocation = geom2dModule.getTextLocation;
lib.clearLocationCache = geom2dModule.clearLocationCache;
lib.getVisibleSegment = geom2dModule.getVisibleSegment;
+lib.findPointOnPath = geom2dModule.findPointOnPath;
var extendModule = require('./extend');
lib.extendFlat = extendModule.extendFlat;
diff --git a/src/plots/cartesian/constants.js b/src/plots/cartesian/constants.js
index b44235855a2..6edfe4fcb9f 100644
--- a/src/plots/cartesian/constants.js
+++ b/src/plots/cartesian/constants.js
@@ -57,18 +57,13 @@ module.exports = {
DFLTRANGEX: [-1, 6],
DFLTRANGEY: [-1, 4],
- // Layers to keep trace types in the right order.
- // from back to front:
- // 1. heatmaps, 2D histos and contour maps
- // 2. bars / 1D histos
- // 3. errorbars for bars and scatter
- // 4. scatter
- // 5. box plots
+ // Layers to keep trace types in the right order
traceLayerClasses: [
'imagelayer',
'maplayer',
'barlayer',
'carpetlayer',
+ 'violinlayer',
'boxlayer',
'scatterlayer'
],
diff --git a/src/plots/cartesian/type_defaults.js b/src/plots/cartesian/type_defaults.js
index a82712763dd..fa68995066f 100644
--- a/src/plots/cartesian/type_defaults.js
+++ b/src/plots/cartesian/type_defaults.js
@@ -75,7 +75,7 @@ function setAutoType(ax, data) {
for(var i = 0; i < data.length; i++) {
trace = data[i];
- if(!Registry.traceIs(trace, 'box') ||
+ if(!Registry.traceIs(trace, 'box-violin') ||
(trace[axLetter + 'axis'] || axLetter) !== id) continue;
if(trace[posLetter] !== undefined) boxPositions.push(trace[posLetter][0]);
@@ -113,7 +113,7 @@ function getBoxPosLetter(trace) {
function isBoxWithoutPositionCoords(trace, axLetter) {
var posLetter = getBoxPosLetter(trace),
- isBox = Registry.traceIs(trace, 'box'),
+ isBox = Registry.traceIs(trace, 'box-violin'),
isCandlestick = Registry.traceIs(trace._fullInput || {}, 'candlestick');
return (
diff --git a/src/plots/plots.js b/src/plots/plots.js
index 03799b4c270..7e0973196f6 100644
--- a/src/plots/plots.js
+++ b/src/plots/plots.js
@@ -1342,7 +1342,6 @@ plots.purge = function(gd) {
delete gd.firstscatter;
delete gd._hmlumcount;
delete gd._hmpixcount;
- delete gd.numboxes;
delete gd._transitionData;
delete gd._transitioning;
delete gd._initialAutoSize;
@@ -2159,8 +2158,12 @@ plots.doCalcdata = function(gd, traces) {
// firstscatter: fill-to-next on the first trace goes to zero
gd.firstscatter = true;
- // how many box plots do we have (in case they're grouped)
- gd.numboxes = 0;
+ // how many box/violins plots do we have (in case they're grouped)
+ fullLayout._numBoxes = 0;
+ fullLayout._numViolins = 0;
+
+ // initialize violin per-scale-group stats container
+ fullLayout._violinScaleGroupStats = {};
// for calculating avg luminosity of heatmaps
gd._hmpixcount = 0;
diff --git a/src/traces/box/calc.js b/src/traces/box/calc.js
index 601fd6a47b0..2f79e0c368c 100644
--- a/src/traces/box/calc.js
+++ b/src/traces/box/calc.js
@@ -15,16 +15,19 @@ var Axes = require('../../plots/cartesian/axes');
// outlier definition based on http://www.physics.csbsju.edu/stats/box2.html
module.exports = function calc(gd, trace) {
+ var fullLayout = gd._fullLayout;
var xa = Axes.getFromId(gd, trace.xaxis || 'x');
var ya = Axes.getFromId(gd, trace.yaxis || 'y');
- var orientation = trace.orientation;
var cd = [];
+ // N.B. violin reuses same Box.calc
+ var numKey = trace.type === 'violin' ? '_numViolins' : '_numBoxes';
+
var i;
var valAxis, valLetter;
var posAxis, posLetter;
- if(orientation === 'h') {
+ if(trace.orientation === 'h') {
valAxis = xa;
valLetter = 'x';
posAxis = ya;
@@ -37,7 +40,7 @@ module.exports = function calc(gd, trace) {
}
var val = valAxis.makeCalcdata(trace, valLetter);
- var pos = getPos(trace, posLetter, posAxis, val, gd.numboxes);
+ var pos = getPos(trace, posLetter, posAxis, val, fullLayout[numKey]);
var dv = Lib.distinctVals(pos);
var posDistinct = dv.vals;
@@ -115,13 +118,16 @@ module.exports = function calc(gd, trace) {
if(cd.length > 0) {
cd[0].t = {
- boxnum: gd.numboxes,
- dPos: dPos
+ num: fullLayout[numKey],
+ dPos: dPos,
+ posLetter: posLetter,
+ valLetter: valLetter
};
- gd.numboxes++;
+
+ fullLayout[numKey]++;
return cd;
} else {
- return [{t: {emptybox: true}}];
+ return [{t: {empty: true}}];
}
};
@@ -130,7 +136,7 @@ module.exports = function calc(gd, trace) {
// so if you want one box
// per trace, set x0 (y0) to the x (y) value or category for this trace
// (or set x (y) to a constant array matching y (x))
-function getPos(trace, posLetter, posAxis, val, numboxes) {
+function getPos(trace, posLetter, posAxis, val, num) {
if(posLetter in trace) {
return posAxis.makeCalcdata(trace, posLetter);
}
@@ -150,7 +156,7 @@ function getPos(trace, posLetter, posAxis, val, numboxes) {
)) {
pos0 = trace.name;
} else {
- pos0 = numboxes;
+ pos0 = num;
}
var pos0c = posAxis.d2c(pos0, 0, trace[posLetter + 'calendar']);
diff --git a/src/traces/box/defaults.js b/src/traces/box/defaults.js
index 64a926e4af7..9ea8e7964af 100644
--- a/src/traces/box/defaults.js
+++ b/src/traces/box/defaults.js
@@ -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');
@@ -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');
@@ -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');
}
@@ -77,4 +88,10 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
}
coerce('hoveron');
+}
+
+module.exports = {
+ supplyDefaults: supplyDefaults,
+ handleSampleDefaults: handleSampleDefaults,
+ handlePointsDefaults: handlePointsDefaults
};
diff --git a/src/traces/box/hover.js b/src/traces/box/hover.js
index d3668a44575..17cc4c1f84c 100644
--- a/src/traces/box/hover.js
+++ b/src/traces/box/hover.js
@@ -14,190 +14,233 @@ var Fx = require('../../components/fx');
var Color = require('../../components/color');
var fillHoverText = require('../scatter/fill_hover_text');
-module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
+function hoverPoints(pointData, xval, yval, hovermode) {
var cd = pointData.cd;
- var xa = pointData.xa;
- var ya = pointData.ya;
-
var trace = cd[0].trace;
var hoveron = trace.hoveron;
- var marker = trace.marker || {};
-
- // output hover points components
var closeBoxData = [];
var closePtData;
- // x/y/effective distance functions
- var dx, dy, distfn;
- // orientation-specific fields
- var posLetter, valLetter, posAxis, valAxis;
- // calcdata item
- var di;
- // loop indices
- var i, j;
if(hoveron.indexOf('boxes') !== -1) {
- var t = cd[0].t;
+ closeBoxData = closeBoxData.concat(hoverOnBoxes(pointData, xval, yval, hovermode));
+ }
- // closest mode: handicap box plots a little relative to others
- // adjust inbox w.r.t. to calculate box size
- var boxDelta = (hovermode === 'closest') ? 2.5 * t.bdPos : t.bdPos;
+ if(hoveron.indexOf('points') !== -1) {
+ closePtData = hoverOnPoints(pointData, xval, yval);
+ }
- if(trace.orientation === 'h') {
- dx = function(di) {
- return Fx.inbox(di.min - xval, di.max - xval);
- };
- dy = function(di) {
- var pos = di.pos + t.bPos - yval;
- return Fx.inbox(pos - boxDelta, pos + boxDelta);
- };
- posLetter = 'y';
- posAxis = ya;
- valLetter = 'x';
- valAxis = xa;
- } else {
- dx = function(di) {
- var pos = di.pos + t.bPos - xval;
- return Fx.inbox(pos - boxDelta, pos + boxDelta);
+ // If there's a point in range and hoveron has points, show the best single point only.
+ // If hoveron has boxes and there's no point in range (or hoveron doesn't have points), show the box stats.
+ if(hovermode === 'closest') {
+ if(closePtData) return [closePtData];
+ return closeBoxData;
+ }
+
+ // Otherwise in compare mode, allow a point AND the box stats to be labeled
+ // If there are multiple boxes in range (ie boxmode = 'overlay') we'll see stats for all of them.
+ if(closePtData) {
+ closeBoxData.push(closePtData);
+ return closeBoxData;
+ }
+ return closeBoxData;
+}
+
+function hoverOnBoxes(pointData, xval, yval, hovermode) {
+ var cd = pointData.cd;
+ var xa = pointData.xa;
+ var ya = pointData.ya;
+ var trace = cd[0].trace;
+ var t = cd[0].t;
+ var isViolin = trace.type === 'violin';
+ var closeBoxData = [];
+
+ var pLetter, vLetter, pAxis, vAxis, vVal, pVal, dx, dy;
+
+ // closest mode: handicap box plots a little relative to others
+ // adjust inbox w.r.t. to calculate box size
+ var boxDelta = (hovermode === 'closest' && !isViolin) ? 2.5 * t.bdPos : t.bdPos;
+ var shiftPos = function(di) { return di.pos + t.bPos - pVal; };
+ var dPos;
+
+ if(isViolin && trace.side !== 'both') {
+ if(trace.side === 'positive') {
+ dPos = function(di) {
+ var pos = shiftPos(di);
+ return Fx.inbox(pos, pos + boxDelta);
};
- dy = function(di) {
- return Fx.inbox(di.min - yval, di.max - yval);
+ }
+ if(trace.side === 'negative') {
+ dPos = function(di) {
+ var pos = shiftPos(di);
+ return Fx.inbox(pos - boxDelta, pos);
};
- posLetter = 'x';
- posAxis = xa;
- valLetter = 'y';
- valAxis = ya;
}
+ } else {
+ dPos = function(di) {
+ var pos = shiftPos(di);
+ return Fx.inbox(pos - boxDelta, pos + boxDelta);
+ };
+ }
- distfn = Fx.getDistanceFunction(hovermode, dx, dy);
- Fx.getClosest(cd, distfn, pointData);
+ var dVal;
- // skip the rest (for this trace) if we didn't find a close point
- // and create the item(s) in closedata for this point
- if(pointData.index !== false) {
- di = cd[pointData.index];
+ if(isViolin) {
+ dVal = function(di) {
+ return Fx.inbox(di.span[0] - vVal, di.span[1] - vVal);
+ };
+ } else {
+ dVal = function(di) {
+ return Fx.inbox(di.min - vVal, di.max - vVal);
+ };
+ }
- var lc = trace.line.color;
- var mc = marker.color;
+ if(trace.orientation === 'h') {
+ vVal = xval;
+ pVal = yval;
+ dx = dVal;
+ dy = dPos;
+ pLetter = 'y';
+ pAxis = ya;
+ vLetter = 'x';
+ vAxis = xa;
+ } else {
+ vVal = yval;
+ pVal = xval;
+ dx = dPos;
+ dy = dVal;
+ pLetter = 'x';
+ pAxis = xa;
+ vLetter = 'y';
+ vAxis = ya;
+ }
- if(Color.opacity(lc) && trace.line.width) pointData.color = lc;
- else if(Color.opacity(mc) && trace.boxpoints) pointData.color = mc;
- else pointData.color = trace.fillcolor;
+ var distfn = Fx.getDistanceFunction(hovermode, dx, dy);
+ Fx.getClosest(cd, distfn, pointData);
- pointData[posLetter + '0'] = posAxis.c2p(di.pos + t.bPos - t.bdPos, true);
- pointData[posLetter + '1'] = posAxis.c2p(di.pos + t.bPos + t.bdPos, true);
+ // skip the rest (for this trace) if we didn't find a close point
+ // and create the item(s) in closedata for this point
+ if(pointData.index === false) return [];
- Axes.tickText(posAxis, posAxis.c2l(di.pos), 'hover').text;
- pointData[posLetter + 'LabelVal'] = di.pos;
+ var di = cd[pointData.index];
+ var lc = trace.line.color;
+ var mc = (trace.marker || {}).color;
- // box plots: each "point" gets many labels
- var usedVals = {};
- var attrs = ['med', 'min', 'q1', 'q3', 'max'];
- var prefixes = ['median', 'min', 'q1', 'q3', 'max'];
+ if(Color.opacity(lc) && trace.line.width) pointData.color = lc;
+ else if(Color.opacity(mc) && trace.boxpoints) pointData.color = mc;
+ else pointData.color = trace.fillcolor;
- if(trace.boxmean) {
- attrs.push('mean');
- prefixes.push(trace.boxmean === 'sd' ? 'mean ± σ' : 'mean');
- }
- if(trace.boxpoints) {
- attrs.push('lf', 'uf');
- prefixes.push('lower fence', 'upper fence');
- }
+ pointData[pLetter + '0'] = pAxis.c2p(di.pos + t.bPos - t.bdPos, true);
+ pointData[pLetter + '1'] = pAxis.c2p(di.pos + t.bPos + t.bdPos, true);
- for(i = 0; i < attrs.length; i++) {
- var attr = attrs[i];
+ Axes.tickText(pAxis, pAxis.c2l(di.pos), 'hover').text;
+ pointData[pLetter + 'LabelVal'] = di.pos;
- if(!(attr in di) || (di[attr] in usedVals)) continue;
- usedVals[di[attr]] = true;
+ // box plots: each "point" gets many labels
+ var usedVals = {};
+ var attrs = ['med', 'min', 'q1', 'q3', 'max'];
+ var prefixes = ['median', 'min', 'q1', 'q3', 'max'];
- // copy out to a new object for each value to label
- var val = di[attr];
- var valPx = valAxis.c2p(val, true);
- var pointData2 = Lib.extendFlat({}, pointData);
+ if(trace.boxmean || (trace.meanline || {}).visible) {
+ attrs.push('mean');
+ prefixes.push(trace.boxmean === 'sd' ? 'mean ± σ' : 'mean');
+ }
+ if(trace.boxpoints || trace.points) {
+ attrs.push('lf', 'uf');
+ prefixes.push('lower fence', 'upper fence');
+ }
- pointData2[valLetter + '0'] = pointData2[valLetter + '1'] = valPx;
- pointData2[valLetter + 'LabelVal'] = val;
- pointData2[valLetter + 'Label'] = prefixes[i] + ': ' + Axes.hoverLabelText(valAxis, val);
+ for(var i = 0; i < attrs.length; i++) {
+ var attr = attrs[i];
- if(attr === 'mean' && ('sd' in di) && trace.boxmean === 'sd') {
- pointData2[valLetter + 'err'] = di.sd;
- }
- // only keep name on the first item (median)
- pointData.name = '';
+ if(!(attr in di) || (di[attr] in usedVals)) continue;
+ usedVals[di[attr]] = true;
- closeBoxData.push(pointData2);
- }
- }
- }
+ // copy out to a new object for each value to label
+ var val = di[attr];
+ var valPx = vAxis.c2p(val, true);
+ var pointData2 = Lib.extendFlat({}, pointData);
- if(hoveron.indexOf('points') !== -1) {
- var xPx = xa.c2p(xval);
- var yPx = ya.c2p(yval);
+ pointData2[vLetter + '0'] = pointData2[vLetter + '1'] = valPx;
+ pointData2[vLetter + 'LabelVal'] = val;
+ pointData2[vLetter + 'Label'] = prefixes[i] + ': ' + Axes.hoverLabelText(vAxis, val);
- dx = function(di) {
- var rad = Math.max(3, di.mrc || 0);
- return Math.max(Math.abs(xa.c2p(di.x) - xPx) - rad, 1 - 3 / rad);
- };
- dy = function(di) {
- var rad = Math.max(3, di.mrc || 0);
- return Math.max(Math.abs(ya.c2p(di.y) - yPx) - rad, 1 - 3 / rad);
- };
- distfn = Fx.quadrature(dx, dy);
+ if(attr === 'mean' && ('sd' in di) && trace.boxmean === 'sd') {
+ pointData2[vLetter + 'err'] = di.sd;
+ }
+ // only keep name on the first item (median)
+ pointData.name = '';
- // show one point per trace
- var ijClosest = false;
- var pt;
+ closeBoxData.push(pointData2);
+ }
- for(i = 0; i < cd.length; i++) {
- di = cd[i];
+ return closeBoxData;
+}
- for(j = 0; j < (di.pts || []).length; j++) {
- pt = di.pts[j];
+function hoverOnPoints(pointData, xval, yval) {
+ var cd = pointData.cd;
+ var xa = pointData.xa;
+ var ya = pointData.ya;
+ var trace = cd[0].trace;
+ var xPx = xa.c2p(xval);
+ var yPx = ya.c2p(yval);
+ var closePtData;
- var newDistance = distfn(pt);
- if(newDistance <= pointData.distance) {
- pointData.distance = newDistance;
- ijClosest = [i, j];
- }
+ var dx = function(di) {
+ var rad = Math.max(3, di.mrc || 0);
+ return Math.max(Math.abs(xa.c2p(di.x) - xPx) - rad, 1 - 3 / rad);
+ };
+ var dy = function(di) {
+ var rad = Math.max(3, di.mrc || 0);
+ return Math.max(Math.abs(ya.c2p(di.y) - yPx) - rad, 1 - 3 / rad);
+ };
+ var distfn = Fx.quadrature(dx, dy);
+
+ // show one point per trace
+ var ijClosest = false;
+ var di, pt;
+
+ for(var i = 0; i < cd.length; i++) {
+ di = cd[i];
+
+ for(var j = 0; j < (di.pts || []).length; j++) {
+ pt = di.pts[j];
+
+ var newDistance = distfn(pt);
+ if(newDistance <= pointData.distance) {
+ pointData.distance = newDistance;
+ ijClosest = [i, j];
}
}
-
- if(ijClosest) {
- di = cd[ijClosest[0]];
- pt = di.pts[ijClosest[1]];
-
- var xc = xa.c2p(pt.x, true);
- var yc = ya.c2p(pt.y, true);
- var rad = pt.mrc || 1;
-
- closePtData = Lib.extendFlat({}, pointData, {
- // corresponds to index in x/y input data array
- index: pt.i,
- color: marker.color,
- name: trace.name,
- x0: xc - rad,
- x1: xc + rad,
- xLabelVal: pt.x,
- y0: yc - rad,
- y1: yc + rad,
- yLabelVal: pt.y
- });
- fillHoverText(pt, trace, closePtData);
- }
- }
-
- // In closest mode, show only one point or stats for one box, and points have priority
- // If there's a point in range and hoveron has points, show the best single point only.
- // If hoveron has boxes and there's no point in range (or hoveron doesn't have points), show the box stats.
- if(hovermode === 'closest') {
- if(closePtData) return [closePtData];
- return closeBoxData;
}
- // Otherwise in compare mode, allow a point AND the box stats to be labeled
- // If there are multiple boxes in range (ie boxmode = 'overlay') we'll see stats for all of them.
- if(closePtData) {
- closeBoxData.push(closePtData);
- return closeBoxData;
- }
- return closeBoxData;
+ if(!ijClosest) return false;
+
+ di = cd[ijClosest[0]];
+ pt = di.pts[ijClosest[1]];
+
+ var xc = xa.c2p(pt.x, true);
+ var yc = ya.c2p(pt.y, true);
+ var rad = pt.mrc || 1;
+
+ closePtData = Lib.extendFlat({}, pointData, {
+ // corresponds to index in x/y input data array
+ index: pt.i,
+ color: (trace.marker || {}).color,
+ name: trace.name,
+ x0: xc - rad,
+ x1: xc + rad,
+ xLabelVal: pt.x,
+ y0: yc - rad,
+ y1: yc + rad,
+ yLabelVal: pt.y
+ });
+ fillHoverText(pt, trace, closePtData);
+
+ return closePtData;
+}
+
+module.exports = {
+ hoverPoints: hoverPoints,
+ hoverOnBoxes: hoverOnBoxes,
+ hoverOnPoints: hoverOnPoints
};
diff --git a/src/traces/box/index.js b/src/traces/box/index.js
index 2294107883b..3d094a2034b 100644
--- a/src/traces/box/index.js
+++ b/src/traces/box/index.js
@@ -12,19 +12,19 @@ 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.setPositions = require('./set_positions').setPositions;
+Box.plot = require('./plot').plot;
Box.style = require('./style');
-Box.hoverPoints = require('./hover');
+Box.hoverPoints = require('./hover').hoverPoints;
Box.selectPoints = require('./select');
Box.moduleType = 'trace';
Box.name = 'box';
Box.basePlotModule = require('../../plots/cartesian');
-Box.categories = ['cartesian', 'symbols', 'oriented', 'box', 'showLegend'];
+Box.categories = ['cartesian', 'symbols', 'oriented', 'box-violin', 'showLegend'];
Box.meta = {
description: [
'In vertical (horizontal) box plots,',
diff --git a/src/traces/box/layout_defaults.js b/src/traces/box/layout_defaults.js
index 3213f703af8..4cd9c2b62e3 100644
--- a/src/traces/box/layout_defaults.js
+++ b/src/traces/box/layout_defaults.js
@@ -8,25 +8,32 @@
'use strict';
-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);
- }
-
- var hasBoxes;
+function _supply(layoutIn, layoutOut, fullData, coerce, traceType) {
+ var hasTraceType;
for(var i = 0; i < fullData.length; i++) {
- if(Registry.traceIs(fullData[i], 'box')) {
- hasBoxes = true;
+ if(fullData[i].type === traceType) {
+ hasTraceType = true;
break;
}
}
- if(!hasBoxes) return;
+ if(!hasTraceType) return;
+
+ coerce(traceType + 'mode');
+ coerce(traceType + 'gap');
+ coerce(traceType + 'groupgap');
+}
+
+function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
+ function coerce(attr, dflt) {
+ return Lib.coerce(layoutIn, layoutOut, layoutAttributes, attr, dflt);
+ }
+ _supply(layoutIn, layoutOut, fullData, coerce, 'box');
+}
- coerce('boxmode');
- coerce('boxgap');
- coerce('boxgroupgap');
+module.exports = {
+ supplyLayoutDefaults: supplyLayoutDefaults,
+ _supply: _supply
};
diff --git a/src/traces/box/plot.js b/src/traces/box/plot.js
index e3fbfd2b90a..e36a863de95 100644
--- a/src/traces/box/plot.js
+++ b/src/traces/box/plot.js
@@ -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;
@@ -49,22 +49,23 @@ module.exports = function plot(gd, plotinfo, cdbox) {
var t = cd0.t;
var trace = cd0.trace;
var sel = cd0.node3 = d3.select(this);
+ var numBoxes = fullLayout._numBoxes;
- var group = (fullLayout.boxmode === 'group' && gd.numboxes > 1);
+ var group = (fullLayout.boxmode === 'group' && numBoxes > 1);
// box half width
- var bdPos = t.dPos * (1 - fullLayout.boxgap) * (1 - fullLayout.boxgroupgap) / (group ? gd.numboxes : 1);
+ var bdPos = t.dPos * (1 - fullLayout.boxgap) * (1 - fullLayout.boxgroupgap) / (group ? numBoxes : 1);
// box center offset
- var bPos = group ? 2 * t.dPos * (-0.5 + (t.boxnum + 0.5) / gd.numboxes) * (1 - fullLayout.boxgap) : 0;
+ var bPos = group ? 2 * t.dPos * (-0.5 + (t.num + 0.5) / numBoxes) * (1 - fullLayout.boxgap) : 0;
// whisker width
var wdPos = bdPos * trace.whiskerwidth;
- if(trace.visible !== true || t.emptybox) {
+ if(trace.visible !== true || t.empty) {
d3.select(this).remove();
return;
}
- // set axis via orientation
var posAxis, valAxis;
+
if(trace.orientation === 'h') {
posAxis = ya;
valAxis = xa;
@@ -76,171 +77,240 @@ module.exports = function plot(gd, plotinfo, cdbox) {
// save the box size and box position for use by hover
t.bPos = bPos;
t.bdPos = bdPos;
-
- // repeatable pseudorandom number generator
- seed();
+ t.wdPos = wdPos;
// boxes and whiskers
- sel.selectAll('path.box')
- .data(Lib.identity)
- .enter().append('path')
- .style('vector-effect', 'non-scaling-stroke')
- .attr('class', 'box')
- .each(function(d) {
- var posc = posAxis.c2p(d.pos + bPos, true),
- pos0 = posAxis.c2p(d.pos + bPos - bdPos, true),
- pos1 = posAxis.c2p(d.pos + bPos + bdPos, true),
- posw0 = posAxis.c2p(d.pos + bPos - wdPos, true),
- posw1 = posAxis.c2p(d.pos + bPos + wdPos, true),
- q1 = valAxis.c2p(d.q1, true),
- q3 = valAxis.c2p(d.q3, true),
- // make sure median isn't identical to either of the
- // quartiles, so we can see it
- m = Lib.constrain(valAxis.c2p(d.med, true),
- Math.min(q1, q3) + 1, Math.max(q1, q3) - 1),
- lf = valAxis.c2p(trace.boxpoints === false ? d.min : d.lf, true),
- uf = valAxis.c2p(trace.boxpoints === false ? d.max : d.uf, true);
-
- if(trace.orientation === 'h') {
- d3.select(this).attr('d',
- 'M' + m + ',' + pos0 + 'V' + pos1 + // median line
- 'M' + q1 + ',' + pos0 + 'V' + pos1 + 'H' + q3 + 'V' + pos0 + 'Z' + // box
- 'M' + q1 + ',' + posc + 'H' + lf + 'M' + q3 + ',' + posc + 'H' + uf + // whiskers
- ((trace.whiskerwidth === 0) ? '' : // whisker caps
- 'M' + lf + ',' + posw0 + 'V' + posw1 + 'M' + uf + ',' + posw0 + 'V' + posw1));
- } else {
- d3.select(this).attr('d',
- 'M' + pos0 + ',' + m + 'H' + pos1 + // median line
- 'M' + pos0 + ',' + q1 + 'H' + pos1 + 'V' + q3 + 'H' + pos0 + 'Z' + // box
- 'M' + posc + ',' + q1 + 'V' + lf + 'M' + posc + ',' + q3 + 'V' + uf + // whiskers
- ((trace.whiskerwidth === 0) ? '' : // whisker caps
- 'M' + posw0 + ',' + lf + 'H' + posw1 + 'M' + posw0 + ',' + uf + 'H' + posw1));
- }
- });
+ plotBoxAndWhiskers(sel, {pos: posAxis, val: valAxis}, trace, t);
// 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;
- }
+ plotPoints(sel, {x: xa, y: ya}, trace, t);
+ }
- // fills in 'x' and 'y' in calcdata 'pts' item
- for(i = 0; i < pts.length; i++) {
- var pt = pts[i];
- var v = pt.v;
+ // draw mean (and stdev diamond) if desired
+ if(trace.boxmean) {
+ plotBoxMean(sel, {pos: posAxis, val: valAxis}, trace, t);
+ }
+ });
+}
- var jitterOffset = trace.jitter ?
- (newJitter * jitterFactors[i] * (rand() - 0.5)) :
- 0;
+function plotBoxAndWhiskers(sel, axes, trace, t) {
+ var posAxis = axes.pos;
+ var valAxis = axes.val;
+ var bPos = t.bPos;
+ var wdPos = t.wdPos || 0;
+ var bPosPxOffset = t.bPosPxOffset || 0;
+ var whiskerWidth = trace.whiskerwidth || 0;
- var posPx = d.pos + bPos + bdPos * (trace.pointpos + jitterOffset);
+ // to support for one-sided box
+ var bdPos0;
+ var bdPos1;
+ if(Array.isArray(t.bdPos)) {
+ bdPos0 = t.bdPos[0];
+ bdPos1 = t.bdPos[1];
+ } else {
+ bdPos0 = t.bdPos;
+ bdPos1 = t.bdPos;
+ }
- if(trace.orientation === 'h') {
- pt.y = posPx;
- pt.x = v;
- } else {
- pt.x = posPx;
- pt.y = v;
- }
+ sel.selectAll('path.box')
+ .data(Lib.identity)
+ .enter().append('path')
+ .style('vector-effect', 'non-scaling-stroke')
+ .attr('class', 'box')
+ .each(function(d) {
+ var pos = d.pos;
+ var posc = posAxis.c2p(pos + bPos, true) + bPosPxOffset;
+ var pos0 = posAxis.c2p(pos + bPos - bdPos0, true) + bPosPxOffset;
+ var pos1 = posAxis.c2p(pos + bPos + bdPos1, true) + bPosPxOffset;
+ var posw0 = posAxis.c2p(pos + bPos - wdPos, true) + bPosPxOffset;
+ var posw1 = posAxis.c2p(pos + bPos + wdPos, true) + bPosPxOffset;
+ var q1 = valAxis.c2p(d.q1, true);
+ var q3 = valAxis.c2p(d.q3, true);
+ // make sure median isn't identical to either of the
+ // quartiles, so we can see it
+ var m = Lib.constrain(
+ valAxis.c2p(d.med, true),
+ Math.min(q1, q3) + 1, Math.max(q1, q3) - 1
+ );
+ var lf = valAxis.c2p(trace.boxpoints === false ? d.min : d.lf, true);
+ var uf = valAxis.c2p(trace.boxpoints === false ? d.max : d.uf, true);
- // tag suspected outliers
- if(trace.boxpoints === 'suspectedoutliers' && v < d.uo && v > d.lo) {
- pt.so = true;
- }
- }
+ if(trace.orientation === 'h') {
+ d3.select(this).attr('d',
+ 'M' + m + ',' + pos0 + 'V' + pos1 + // median line
+ 'M' + q1 + ',' + pos0 + 'V' + pos1 + 'H' + q3 + 'V' + pos0 + 'Z' + // box
+ 'M' + q1 + ',' + posc + 'H' + lf + 'M' + q3 + ',' + posc + 'H' + uf + // whiskers
+ ((whiskerWidth === 0) ? '' : // whisker caps
+ 'M' + lf + ',' + posw0 + 'V' + posw1 + 'M' + uf + ',' + posw0 + 'V' + posw1));
+ } else {
+ d3.select(this).attr('d',
+ 'M' + pos0 + ',' + m + 'H' + pos1 + // median line
+ 'M' + pos0 + ',' + q1 + 'H' + pos1 + 'V' + q3 + 'H' + pos0 + 'Z' + // box
+ 'M' + posc + ',' + q1 + 'V' + lf + 'M' + posc + ',' + q3 + 'V' + uf + // whiskers
+ ((whiskerWidth === 0) ? '' : // whisker caps
+ 'M' + posw0 + ',' + lf + 'H' + posw1 + 'M' + posw0 + ',' + uf + 'H' + posw1));
+ }
+ });
+}
- return pts;
- })
- .enter().append('path')
- .classed('point', true)
- .call(Drawing.translatePoints, xa, ya);
- }
+function plotPoints(sel, axes, trace, t) {
+ var xa = axes.x;
+ var ya = axes.y;
+ var bdPos = t.bdPos;
+ var bPos = t.bPos;
- // draw mean (and stdev diamond) if desired
- if(trace.boxmean) {
- sel.selectAll('path.mean')
- .data(Lib.identity)
- .enter().append('path')
- .attr('class', 'mean')
- .style({
- fill: 'none',
- 'vector-effect': 'non-scaling-stroke'
- })
- .each(function(d) {
- var posc = posAxis.c2p(d.pos + bPos, true),
- pos0 = posAxis.c2p(d.pos + bPos - bdPos, true),
- pos1 = posAxis.c2p(d.pos + bPos + bdPos, true),
- m = valAxis.c2p(d.mean, true),
- sl = valAxis.c2p(d.mean - d.sd, true),
- sh = valAxis.c2p(d.mean + d.sd, true);
- if(trace.orientation === 'h') {
- d3.select(this).attr('d',
- 'M' + m + ',' + pos0 + 'V' + pos1 +
- ((trace.boxmean !== 'sd') ? '' :
- 'm0,0L' + sl + ',' + posc + 'L' + m + ',' + pos0 + 'L' + sh + ',' + posc + 'Z'));
+ // to support violin points
+ var mode = trace.boxpoints || trace.points;
+
+ // 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 {
- d3.select(this).attr('d',
- 'M' + pos0 + ',' + m + 'H' + pos1 +
- ((trace.boxmean !== 'sd') ? '' :
- 'm0,0L' + posc + ',' + sl + 'L' + pos0 + ',' + m + 'L' + posc + ',' + sh + 'Z'));
+ } 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);
+}
+
+function plotBoxMean(sel, axes, trace, t) {
+ var posAxis = axes.pos;
+ var valAxis = axes.val;
+ var bPos = t.bPos;
+ var bPosPxOffset = t.bPosPxOffset || 0;
+
+ // to support for one-sided box
+ var bdPos0;
+ var bdPos1;
+ if(Array.isArray(t.bdPos)) {
+ bdPos0 = t.bdPos[0];
+ bdPos1 = t.bdPos[1];
+ } else {
+ bdPos0 = t.bdPos;
+ bdPos1 = t.bdPos;
+ }
+
+ sel.selectAll('path.mean')
+ .data(Lib.identity)
+ .enter().append('path')
+ .attr('class', 'mean')
+ .style({
+ fill: 'none',
+ 'vector-effect': 'non-scaling-stroke'
+ })
+ .each(function(d) {
+ var posc = posAxis.c2p(d.pos + bPos, true) + bPosPxOffset;
+ var pos0 = posAxis.c2p(d.pos + bPos - bdPos0, true) + bPosPxOffset;
+ var pos1 = posAxis.c2p(d.pos + bPos + bdPos1, true) + bPosPxOffset;
+ var m = valAxis.c2p(d.mean, true);
+ var sl = valAxis.c2p(d.mean - d.sd, true);
+ var sh = valAxis.c2p(d.mean + d.sd, true);
+
+ if(trace.orientation === 'h') {
+ d3.select(this).attr('d',
+ 'M' + m + ',' + pos0 + 'V' + pos1 +
+ (trace.boxmean === 'sd' ?
+ 'm0,0L' + sl + ',' + posc + 'L' + m + ',' + pos0 + 'L' + sh + ',' + posc + 'Z' :
+ '')
+ );
+ } else {
+ d3.select(this).attr('d',
+ 'M' + pos0 + ',' + m + 'H' + pos1 +
+ (trace.boxmean === 'sd' ?
+ 'm0,0L' + posc + ',' + sl + 'L' + pos0 + ',' + m + 'L' + posc + ',' + sh + 'Z' :
+ '')
+ );
+ }
+ });
+}
+
+module.exports = {
+ plot: plot,
+ plotBoxAndWhiskers: plotBoxAndWhiskers,
+ plotPoints: plotPoints,
+ plotBoxMean: plotBoxMean
};
diff --git a/src/traces/box/set_positions.js b/src/traces/box/set_positions.js
index 30580031d4b..f0a6221b638 100644
--- a/src/traces/box/set_positions.js
+++ b/src/traces/box/set_positions.js
@@ -8,44 +8,37 @@
'use strict';
-var Registry = require('../../registry');
var Axes = require('../../plots/cartesian/axes');
var Lib = require('../../lib');
+var orientations = ['v', 'h'];
-module.exports = function setPositions(gd, plotinfo) {
- var fullLayout = gd._fullLayout,
- xa = plotinfo.xaxis,
- ya = plotinfo.yaxis,
- orientations = ['v', 'h'];
- var posAxis, i, j, k;
+function setPositions(gd, plotinfo) {
+ var calcdata = gd.calcdata;
+ var xa = plotinfo.xaxis;
+ var ya = plotinfo.yaxis;
- for(i = 0; i < orientations.length; ++i) {
- var orientation = orientations[i],
- boxlist = [],
- boxpointlist = [],
- minPad = 0,
- maxPad = 0,
- cd,
- t,
- trace;
-
- // set axis via orientation
- if(orientation === 'h') posAxis = ya;
- else posAxis = xa;
+ for(var i = 0; i < orientations.length; i++) {
+ var orientation = orientations[i];
+ var posAxis = orientation === 'h' ? ya : xa;
+ var boxList = [];
+ var minPad = 0;
+ var maxPad = 0;
// make list of boxes
- for(j = 0; j < gd.calcdata.length; ++j) {
- cd = gd.calcdata[j];
- t = cd[0].t;
- trace = cd[0].trace;
+ for(var j = 0; j < calcdata.length; j++) {
+ var cd = calcdata[j];
+ var t = cd[0].t;
+ var trace = cd[0].trace;
- if(trace.visible === true && Registry.traceIs(trace, 'box') &&
- !t.emptybox &&
+ if(trace.visible === true && trace.type === 'box' &&
+ !t.empty &&
trace.orientation === orientation &&
trace.xaxis === xa._id &&
- trace.yaxis === ya._id) {
- boxlist.push(j);
+ trace.yaxis === ya._id
+ ) {
+ boxList.push(j);
+
if(trace.boxpoints !== false) {
minPad = Math.max(minPad, trace.jitter - trace.pointpos - 1);
maxPad = Math.max(maxPad, trace.jitter + trace.pointpos - 1);
@@ -53,40 +46,64 @@ module.exports = function setPositions(gd, plotinfo) {
}
}
- // make list of box points
- for(j = 0; j < boxlist.length; j++) {
- cd = gd.calcdata[boxlist[j]];
- for(k = 0; k < cd.length; k++) boxpointlist.push(cd[k].pos);
- }
- if(!boxpointlist.length) continue;
-
- // box plots - update dPos based on multiple traces
- // and then use for posAxis autorange
+ setPositionOffset('box', gd, boxList, posAxis, [minPad, maxPad]);
+ }
+}
- var boxdv = Lib.distinctVals(boxpointlist),
- dPos = boxdv.minDiff / 2;
+function setPositionOffset(traceType, gd, boxList, posAxis, pad) {
+ var calcdata = gd.calcdata;
+ var fullLayout = gd._fullLayout;
+ var pointList = [];
- // if there's no duplication of x points,
- // disable 'group' mode by setting numboxes=1
- if(boxpointlist.length === boxdv.vals.length) gd.numboxes = 1;
+ // N.B. reused in violin
+ var numKey = traceType === 'violin' ? '_numViolins' : '_numBoxes';
- // check for forced minimum dtick
- Axes.minDtick(posAxis, boxdv.minDiff, boxdv.vals[0], true);
+ var i, j, calcTrace;
- // set the width of all boxes
- for(i = 0; i < boxlist.length; i++) {
- var boxListIndex = boxlist[i];
- gd.calcdata[boxListIndex][0].t.dPos = dPos;
+ // make list of box points
+ for(i = 0; i < boxList.length; i++) {
+ calcTrace = calcdata[boxList[i]];
+ for(j = 0; j < calcTrace.length; j++) {
+ pointList.push(calcTrace[j].pos);
}
+ }
+
+ if(!pointList.length) return;
- // autoscale the x axis - including space for points if they're off the side
- // TODO: this will overdo it if the outermost boxes don't have
- // their points as far out as the other boxes
- var padfactor = (1 - fullLayout.boxgap) * (1 - fullLayout.boxgroupgap) *
- dPos / gd.numboxes;
- Axes.expand(posAxis, boxdv.vals, {
- vpadminus: dPos + minPad * padfactor,
- vpadplus: dPos + maxPad * padfactor
- });
+ // box plots - update dPos based on multiple traces
+ // and then use for posAxis autorange
+ var boxdv = Lib.distinctVals(pointList);
+ var dPos = boxdv.minDiff / 2;
+
+ // if there's no duplication of x points,
+ // disable 'group' mode by setting counter to 1
+ if(pointList.length === boxdv.vals.length) {
+ fullLayout[numKey] = 1;
+ }
+
+ // check for forced minimum dtick
+ Axes.minDtick(posAxis, boxdv.minDiff, boxdv.vals[0], true);
+
+ // set the width of all boxes
+ for(i = 0; i < boxList.length; i++) {
+ calcTrace = calcdata[boxList[i]];
+ calcTrace[0].t.dPos = dPos;
}
+
+ var gap = fullLayout[traceType + 'gap'];
+ var groupgap = fullLayout[traceType + 'groupgap'];
+ var padfactor = (1 - gap) * (1 - groupgap) * dPos / fullLayout[numKey];
+
+ // autoscale the x axis - including space for points if they're off the side
+ // TODO: this will overdo it if the outermost boxes don't have
+ // their points as far out as the other boxes
+ Axes.expand(posAxis, boxdv.vals, {
+ vpadminus: dPos + pad[0] * padfactor,
+ vpadplus: dPos + pad[1] * padfactor
+ });
+}
+
+module.exports = {
+ setPositions: setPositions,
+ setPositionOffset: setPositionOffset
};
diff --git a/src/traces/contour/hover.js b/src/traces/contour/hover.js
index d53393d9ed8..0bf4fdb4884 100644
--- a/src/traces/contour/hover.js
+++ b/src/traces/contour/hover.js
@@ -11,7 +11,6 @@
var heatmapHoverPoints = require('../heatmap/hover');
-
-module.exports = function hoverPoints(pointData, xval, yval, hovermode) {
- return heatmapHoverPoints(pointData, xval, yval, hovermode, true);
+module.exports = function hoverPoints(pointData, xval, yval, hovermode, hoverLayer) {
+ return heatmapHoverPoints(pointData, xval, yval, hovermode, hoverLayer, true);
};
diff --git a/src/traces/heatmap/hover.js b/src/traces/heatmap/hover.js
index 310f92c0b84..f8ccaaf0bd9 100644
--- a/src/traces/heatmap/hover.js
+++ b/src/traces/heatmap/hover.js
@@ -15,7 +15,7 @@ var Axes = require('../../plots/cartesian/axes');
var MAXDIST = Fx.constants.MAXDIST;
-module.exports = function hoverPoints(pointData, xval, yval, hovermode, contour) {
+module.exports = function hoverPoints(pointData, xval, yval, hovermode, hoverLayer, contour) {
// never let a heatmap override another type as closest point
if(pointData.distance < MAXDIST) return;
diff --git a/src/traces/histogram2d/hover.js b/src/traces/histogram2d/hover.js
index 87972380b61..ccce7d3d712 100644
--- a/src/traces/histogram2d/hover.js
+++ b/src/traces/histogram2d/hover.js
@@ -12,8 +12,8 @@
var heatmapHover = require('../heatmap/hover');
var hoverLabelText = require('../../plots/cartesian/axes').hoverLabelText;
-module.exports = function hoverPoints(pointData, xval, yval, hovermode, contour) {
- var pts = heatmapHover(pointData, xval, yval, hovermode, contour);
+module.exports = function hoverPoints(pointData, xval, yval, hovermode, hoverLayer, contour) {
+ var pts = heatmapHover(pointData, xval, yval, hovermode, hoverLayer, contour);
if(!pts) return;
diff --git a/src/traces/violin/attributes.js b/src/traces/violin/attributes.js
new file mode 100644
index 00000000000..6f7540cf7a2
--- /dev/null
+++ b/src/traces/violin/attributes.js
@@ -0,0 +1,244 @@
+/**
+* Copyright 2012-2017, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+'use strict';
+
+var boxAttrs = require('../box/attributes');
+var extendFlat = require('../../lib/extend').extendFlat;
+
+module.exports = {
+ y: boxAttrs.y,
+ x: boxAttrs.x,
+ x0: boxAttrs.x0,
+ y0: boxAttrs.y0,
+ name: boxAttrs.name,
+ orientation: extendFlat({}, boxAttrs.orientation, {
+ description: [
+ 'Sets the orientation of the violin(s).',
+ 'If *v* (*h*), the distribution is visualized along',
+ 'the vertical (horizontal).'
+ ].join(' ')
+ }),
+
+ bandwidth: {
+ valType: 'number',
+ min: 0,
+ role: 'info',
+ editType: 'calc',
+ description: [
+ 'Sets the bandwidth used to compute the kernel density estimate.',
+ 'By default, the bandwidth is determined by Silverman\'s rule of thumb.'
+ ].join(' ')
+ },
+
+ scalegroup: {
+ valType: 'string',
+ role: 'info',
+ dflt: '',
+ editType: 'calc',
+ description: [
+ 'If there are multiple violins that should be sized according to',
+ 'to some metric (see `scalemode`), link them by providing a non-empty group id here',
+ 'shared by every trace in the same group.'
+ ].join(' ')
+ },
+ scalemode: {
+ valType: 'enumerated',
+ values: ['width', 'count'],
+ dflt: 'width',
+ role: 'info',
+ editType: 'calc',
+ description: [
+ 'Sets the metric by which the width of each violin is determined.',
+ '*width* means each violin has the same (max) width',
+ '*count* means the violins are scaled by the number of sample points making',
+ 'up each violin.'
+ ].join('')
+ },
+
+ spanmode: {
+ valType: 'enumerated',
+ values: ['soft', 'hard', 'manual'],
+ dflt: 'soft',
+ role: 'info',
+ editType: 'calc',
+ description: [
+ 'Sets the method by which the span in data space where the density function will be computed.',
+ '*soft* means the span goes from the sample\'s minimum value minus two bandwidths',
+ 'to the sample\'s maximum value plus two bandwidths.',
+ '*hard* means the span goes from the sample\'s minimum to its maximum value.',
+ 'For custom span settings, use mode *manual* and fill in the `span` attribute.'
+ ].join(' ')
+ },
+ span: {
+ valType: 'info_array',
+ items: [
+ {valType: 'any', editType: 'calc'},
+ {valType: 'any', editType: 'calc'}
+ ],
+ role: 'info',
+ editType: 'calc',
+ description: [
+ 'Sets the span in data space for which the density function will be computed.',
+ 'Has an effect only when `spanmode` is set to *manual*.'
+ ].join(' ')
+ },
+
+ line: {
+ color: {
+ valType: 'color',
+ role: 'style',
+ editType: 'style',
+ description: 'Sets the color of line bounding the violin(s).'
+ },
+ width: {
+ valType: 'number',
+ role: 'style',
+ min: 0,
+ dflt: 2,
+ editType: 'style',
+ description: 'Sets the width (in px) of line bounding the violin(s).'
+ },
+ editType: 'plot'
+ },
+ fillcolor: boxAttrs.fillcolor,
+
+ points: extendFlat({}, boxAttrs.boxpoints, {
+ description: [
+ 'If *outliers*, only the sample points lying outside the whiskers',
+ 'are shown',
+ 'If *suspectedoutliers*, the outlier points are shown and',
+ 'points either less than 4*Q1-3*Q3 or greater than 4*Q3-3*Q1',
+ 'are highlighted (see `outliercolor`)',
+ 'If *all*, all sample points are shown',
+ 'If *false*, only the violins are shown with no sample points'
+ ].join(' ')
+ }),
+ jitter: extendFlat({}, boxAttrs.jitter, {
+ description: [
+ 'Sets the amount of jitter in the sample points drawn.',
+ 'If *0*, the sample points align along the distribution axis.',
+ 'If *1*, the sample points are drawn in a random jitter of width',
+ 'equal to the width of the violins.'
+ ].join(' ')
+ }),
+ pointpos: extendFlat({}, boxAttrs.pointpos, {
+ description: [
+ 'Sets the position of the sample points in relation to the violins.',
+ 'If *0*, the sample points are places over the center of the violins.',
+ 'Positive (negative) values correspond to positions to the',
+ 'right (left) for vertical violins and above (below) for horizontal violins.'
+ ].join(' ')
+ }),
+ marker: boxAttrs.marker,
+ text: boxAttrs.text,
+
+ box: {
+ visible: {
+ valType: 'boolean',
+ dflt: false,
+ role: 'info',
+ editType: 'plot',
+ description: [
+ 'Determines if an miniature box plot is drawn inside the violins. '
+ ].join(' ')
+ },
+ width: {
+ valType: 'number',
+ min: 0,
+ max: 1,
+ dflt: 0.25,
+ role: 'info',
+ editType: 'plot',
+ description: [
+ 'Sets the width of the inner box plots relative to',
+ 'the violins\' width.',
+ 'For example, with 1, the inner box plots are as wide as the violins.'
+ ].join(' ')
+ },
+ fillcolor: {
+ valType: 'color',
+ role: 'style',
+ editType: 'style',
+ description: 'Sets the inner box plot fill color.'
+ },
+ line: {
+ color: {
+ valType: 'color',
+ role: 'style',
+ editType: 'style',
+ description: 'Sets the inner box plot bounding line color.'
+ },
+ width: {
+ valType: 'number',
+ min: 0,
+ role: 'style',
+ editType: 'style',
+ description: 'Sets the inner box plot bounding line width.'
+ },
+ editType: 'style'
+ },
+ editType: 'plot'
+ },
+
+ meanline: {
+ visible: {
+ valType: 'boolean',
+ dflt: false,
+ role: 'info',
+ editType: 'plot',
+ description: [
+ 'Determines if a line corresponding to the sample\'s mean is shown',
+ 'inside the violins.',
+ 'If `box.visible` is turned on, the mean line is drawn inside the inner box.',
+ 'Otherwise, the mean line is drawn from one side of the violin to other.'
+ ].join(' ')
+ },
+ color: {
+ valType: 'color',
+ role: 'style',
+ editType: 'style',
+ description: 'Sets the mean line color.'
+ },
+ width: {
+ valType: 'number',
+ min: 0,
+ role: 'style',
+ editType: 'style',
+ description: 'Sets the mean line width.'
+ },
+ editType: 'plot'
+ },
+
+ side: {
+ valType: 'enumerated',
+ values: ['both', 'positive', 'negative'],
+ dflt: 'both',
+ role: 'info',
+ editType: 'plot',
+ description: [
+ 'Determines on which side of the position value the density function making up',
+ 'one half of a violin is plotted.',
+ 'Useful when comparing two violin traces under *overlay* mode, where one trace',
+ 'has `side` set to *positive* and the other to *negative*.'
+ ].join(' ')
+ },
+
+ hoveron: {
+ valType: 'flaglist',
+ flags: ['violins', 'points', 'kde'],
+ dflt: 'violins+points+kde',
+ extras: ['all'],
+ role: 'info',
+ editType: 'style',
+ description: [
+ 'Do the hover effects highlight individual violins',
+ 'or sample points or the kernel density estimate or any combination of them?'
+ ].join(' ')
+ }
+};
diff --git a/src/traces/violin/calc.js b/src/traces/violin/calc.js
new file mode 100644
index 00000000000..e4b0332824e
--- /dev/null
+++ b/src/traces/violin/calc.js
@@ -0,0 +1,115 @@
+/**
+* Copyright 2012-2017, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+'use strict';
+
+var Lib = require('../../lib');
+var Axes = require('../../plots/cartesian/axes');
+var boxCalc = require('../box/calc');
+var helpers = require('./helpers');
+var BADNUM = require('../../constants/numerical').BADNUM;
+
+module.exports = function calc(gd, trace) {
+ var cd = boxCalc(gd, trace);
+
+ if(cd[0].t.empty) return cd;
+
+ var fullLayout = gd._fullLayout;
+ var valAxis = Axes.getFromId(
+ gd,
+ trace[trace.orientation === 'h' ? 'xaxis' : 'yaxis']
+ );
+
+ var violinScaleGroupStats = fullLayout._violinScaleGroupStats;
+ var scaleGroup = trace.scalegroup;
+ var groupStats = violinScaleGroupStats[scaleGroup];
+ if(!groupStats) {
+ groupStats = violinScaleGroupStats[scaleGroup] = {
+ maxWidth: 0,
+ maxCount: 0
+ };
+ }
+
+ for(var i = 0; i < cd.length; i++) {
+ var cdi = cd[i];
+ var vals = cdi.pts.map(helpers.extractVal);
+ var len = vals.length;
+
+ // sample standard deviation
+ var ssd = Lib.stdev(vals, len - 1, cdi.mean);
+ var bandwidthDflt = ruleOfThumbBandwidth(vals, ssd, cdi.q3 - cdi.q1);
+ var bandwidth = cdi.bandwidth = trace.bandwidth || bandwidthDflt;
+ var span = cdi.span = calcSpan(trace, cdi, valAxis, bandwidth);
+
+ // step that well covers the bandwidth and is multiple of span distance
+ var dist = span[1] - span[0];
+ var n = Math.ceil(dist / (Math.min(bandwidthDflt, bandwidth) / 3));
+ var step = dist / n;
+
+ if(!isFinite(step) || !isFinite(n)) {
+ Lib.error('Something went wrong with computing the violin span');
+ cd[0].t.empty = true;
+ return cd;
+ }
+
+ var kde = helpers.makeKDE(cdi, trace, vals);
+ cdi.density = new Array(n);
+
+ for(var k = 0, t = span[0]; t < (span[1] + step / 2); k++, t += step) {
+ var v = kde(t);
+ groupStats.maxWidth = Math.max(groupStats.maxWidth, v);
+ cdi.density[k] = {v: v, t: t};
+ }
+
+ Axes.expand(valAxis, span, {padded: true});
+ groupStats.maxCount = Math.max(groupStats.maxCount, vals.length);
+ }
+
+ return cd;
+};
+
+// Default to Silveman's rule of thumb:
+// - https://stats.stackexchange.com/a/6671
+// - https://en.wikipedia.org/wiki/Kernel_density_estimation#A_rule-of-thumb_bandwidth_estimator
+// - https://github.com/statsmodels/statsmodels/blob/master/statsmodels/nonparametric/bandwidths.py
+function ruleOfThumbBandwidth(vals, ssd, iqr) {
+ var a = Math.min(ssd, iqr / 1.349);
+ return 1.059 * a * Math.pow(vals.length, -0.2);
+}
+
+function calcSpan(trace, cdi, valAxis, bandwidth) {
+ var spanmode = trace.spanmode;
+ var spanIn = trace.span || [];
+ var spanTight = [cdi.min, cdi.max];
+ var spanLoose = [cdi.min - 2 * bandwidth, cdi.max + 2 * bandwidth];
+ var spanOut;
+
+ function calcSpanItem(index) {
+ var s = spanIn[index];
+ var sc = valAxis.d2c(s, 0, trace[cdi.valLetter + 'calendar']);
+ return sc === BADNUM ? spanLoose[index] : sc;
+ }
+
+ if(spanmode === 'soft') {
+ spanOut = spanLoose;
+ } else if(spanmode === 'hard') {
+ spanOut = spanTight;
+ } else {
+ spanOut = [calcSpanItem(0), calcSpanItem(1)];
+ }
+
+ // to reuse the equal-range-item block
+ var dummyAx = {
+ type: 'linear',
+ range: spanOut
+ };
+ Axes.setConvert(dummyAx);
+ dummyAx.cleanRange();
+
+ return spanOut;
+}
diff --git a/src/traces/violin/defaults.js b/src/traces/violin/defaults.js
new file mode 100644
index 00000000000..e47480c1b9b
--- /dev/null
+++ b/src/traces/violin/defaults.js
@@ -0,0 +1,55 @@
+/**
+* Copyright 2012-2017, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+'use strict';
+
+var Lib = require('../../lib');
+var Color = require('../../components/color');
+
+var boxDefaults = require('../box/defaults');
+var attributes = require('./attributes');
+
+module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
+ function coerce(attr, dflt) {
+ return Lib.coerce(traceIn, traceOut, attributes, attr, dflt);
+ }
+ function coerce2(attr, dflt) {
+ return Lib.coerce2(traceIn, traceOut, attributes, attr, dflt);
+ }
+
+ boxDefaults.handleSampleDefaults(traceIn, traceOut, coerce, layout);
+ if(traceOut.visible === false) return;
+
+ coerce('bandwidth');
+ coerce('scalegroup', traceOut.name);
+ coerce('scalemode');
+ coerce('side');
+
+ var span = coerce('span');
+ var spanmodeDflt;
+ if(Array.isArray(span)) spanmodeDflt = 'manual';
+ coerce('spanmode', spanmodeDflt);
+
+ var lineColor = coerce('line.color', (traceIn.marker || {}).color || defaultColor);
+ var lineWidth = coerce('line.width');
+ var fillColor = coerce('fillcolor', Color.addOpacity(traceOut.line.color, 0.5));
+
+ boxDefaults.handlePointsDefaults(traceIn, traceOut, coerce, {prefix: ''});
+
+ var boxWidth = coerce2('box.width');
+ var boxFillColor = coerce2('box.fillcolor', fillColor);
+ var boxLineColor = coerce2('box.line.color', lineColor);
+ var boxLineWidth = coerce2('box.line.width', lineWidth);
+ var boxVisible = coerce('box.visible', Boolean(boxWidth || boxFillColor || boxLineColor || boxLineWidth));
+ if(!boxVisible) delete traceOut.box;
+
+ var meanLineColor = coerce2('meanline.color', lineColor);
+ var meanLineWidth = coerce2('meanline.width', lineWidth);
+ var meanLineVisible = coerce('meanline.visible', Boolean(meanLineColor || meanLineWidth));
+ if(!meanLineVisible) delete traceOut.meanline;
+};
diff --git a/src/traces/violin/helpers.js b/src/traces/violin/helpers.js
new file mode 100644
index 00000000000..6e191793049
--- /dev/null
+++ b/src/traces/violin/helpers.js
@@ -0,0 +1,71 @@
+/**
+* Copyright 2012-2017, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+'use strict';
+
+var Lib = require('../../lib');
+
+// Maybe add kernels more down the road,
+// but note that the default `spanmode: 'soft'` bounds might have
+// to become kernel-dependent
+var kernels = {
+ gaussian: function(v) {
+ return (1 / Math.sqrt(2 * Math.PI)) * Math.exp(-0.5 * v * v);
+ }
+};
+
+exports.makeKDE = function(calcItem, trace, vals) {
+ var len = vals.length;
+ var kernel = kernels.gaussian;
+ var bandwidth = calcItem.bandwidth;
+ var factor = 1 / (len * bandwidth);
+
+ // don't use Lib.aggNums to skip isNumeric checks
+ return function(x) {
+ var sum = 0;
+ for(var i = 0; i < len; i++) {
+ sum += kernel((x - vals[i]) / bandwidth);
+ }
+ return factor * sum;
+ };
+};
+
+exports.getPositionOnKdePath = function(calcItem, trace, valuePx) {
+ var posLetter, valLetter;
+
+ if(trace.orientation === 'h') {
+ posLetter = 'y';
+ valLetter = 'x';
+ } else {
+ posLetter = 'x';
+ valLetter = 'y';
+ }
+
+ var pointOnPath = Lib.findPointOnPath(
+ calcItem.path,
+ valuePx,
+ valLetter,
+ {pathLength: calcItem.pathLength}
+ );
+
+ var posCenterPx = calcItem.posCenterPx;
+ var posOnPath0 = pointOnPath[posLetter];
+ var posOnPath1 = trace.side === 'both' ?
+ 2 * posCenterPx - posOnPath0 :
+ posCenterPx;
+
+ return [posOnPath0, posOnPath1];
+};
+
+exports.getKdeValue = function(calcItem, trace, valueDist) {
+ var vals = calcItem.pts.map(exports.extractVal);
+ var kde = exports.makeKDE(calcItem, trace, vals);
+ return kde(valueDist) / calcItem.posDensityScale;
+};
+
+exports.extractVal = function(o) { return o.v; };
diff --git a/src/traces/violin/hover.js b/src/traces/violin/hover.js
new file mode 100644
index 00000000000..32ea61ba63a
--- /dev/null
+++ b/src/traces/violin/hover.js
@@ -0,0 +1,99 @@
+/**
+* Copyright 2012-2017, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+'use strict';
+
+var Lib = require('../../lib');
+var Axes = require('../../plots/cartesian/axes');
+var boxHoverPoints = require('../box/hover');
+var helpers = require('./helpers');
+
+module.exports = function hoverPoints(pointData, xval, yval, hovermode, hoverLayer) {
+ var cd = pointData.cd;
+ var trace = cd[0].trace;
+ var hoveron = trace.hoveron;
+ var hasHoveronViolins = hoveron.indexOf('violins') !== -1;
+ var hasHoveronKDE = hoveron.indexOf('kde') !== -1;
+ var closeData = [];
+ var closePtData;
+ var violinLineAttrs;
+
+ if(hasHoveronViolins || hasHoveronKDE) {
+ var closeBoxData = boxHoverPoints.hoverOnBoxes(pointData, xval, yval, hovermode);
+
+ if(hasHoveronViolins) {
+ closeData = closeData.concat(closeBoxData);
+ }
+
+ if(hasHoveronKDE && closeBoxData.length > 0) {
+ var xa = pointData.xa;
+ var ya = pointData.ya;
+ var pLetter, vLetter, pAxis, vAxis, vVal;
+
+ if(trace.orientation === 'h') {
+ vVal = xval;
+ pLetter = 'y';
+ pAxis = ya;
+ vLetter = 'x';
+ vAxis = xa;
+ } else {
+ vVal = yval;
+ pLetter = 'x';
+ pAxis = xa;
+ vLetter = 'y';
+ vAxis = ya;
+ }
+
+ var di = cd[pointData.index];
+
+ if(vVal >= di.span[0] && vVal <= di.span[1]) {
+ var kdePointData = Lib.extendFlat({}, pointData);
+ var vValPx = vAxis.c2p(vVal, true);
+ var kdeVal = helpers.getKdeValue(di, trace, vVal);
+ var pOnPath = helpers.getPositionOnKdePath(di, trace, vValPx);
+ var paOffset = pAxis._offset;
+ var paLength = pAxis._length;
+
+ kdePointData[pLetter + '0'] = pOnPath[0];
+ kdePointData[pLetter + '1'] = pOnPath[1];
+ kdePointData[vLetter + '0'] = kdePointData[vLetter + '1'] = vValPx;
+ kdePointData[vLetter + 'Label'] = vLetter + ': ' + Axes.hoverLabelText(vAxis, vVal) + ', kde: ' + kdeVal.toFixed(3);
+ closeData.push(kdePointData);
+
+ violinLineAttrs = {stroke: pointData.color};
+ violinLineAttrs[pLetter + '1'] = Lib.constrain(paOffset + pOnPath[0], paOffset, paOffset + paLength);
+ violinLineAttrs[pLetter + '2'] = Lib.constrain(paOffset + pOnPath[1], paOffset, paOffset + paLength);
+ violinLineAttrs[vLetter + '1'] = violinLineAttrs[vLetter + '2'] = vAxis._offset + vValPx;
+ }
+ }
+ }
+
+ if(hoveron.indexOf('points') !== -1) {
+ closePtData = boxHoverPoints.hoverOnPoints(pointData, xval, yval);
+ }
+
+ // update violin line (if any)
+ var violinLine = hoverLayer.selectAll('.violinline-' + trace.uid)
+ .data(violinLineAttrs ? [0] : []);
+ violinLine.enter().append('line')
+ .classed('violinline-' + trace.uid, true)
+ .attr('stroke-width', 1.5);
+ violinLine.exit().remove();
+ violinLine.attr(violinLineAttrs);
+
+ // same combine logic as box hoverPoints
+ if(hovermode === 'closest') {
+ if(closePtData) return [closePtData];
+ return closeData;
+ }
+ if(closePtData) {
+ closeData.push(closePtData);
+ return closeData;
+ }
+ return closeData;
+};
diff --git a/src/traces/violin/index.js b/src/traces/violin/index.js
new file mode 100644
index 00000000000..75212b5335e
--- /dev/null
+++ b/src/traces/violin/index.js
@@ -0,0 +1,38 @@
+/**
+* Copyright 2012-2017, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+'use strict';
+
+module.exports = {
+ attributes: require('./attributes'),
+ layoutAttributes: require('./layout_attributes'),
+ supplyDefaults: require('./defaults'),
+ supplyLayoutDefaults: require('./layout_defaults'),
+ calc: require('./calc'),
+ setPositions: require('./set_positions'),
+ plot: require('./plot'),
+ style: require('./style'),
+ hoverPoints: require('./hover'),
+ selectPoints: require('../box/select'),
+
+ moduleType: 'trace',
+ name: 'violin',
+ basePlotModule: require('../../plots/cartesian'),
+ categories: ['cartesian', 'symbols', 'oriented', 'box-violin', 'showLegend'],
+ meta: {
+ description: [
+ 'In vertical (horizontal) violin plots,',
+ 'statistics are computed using `y` (`x`) values.',
+ 'By supplying an `x` (`y`) array, one violin per distinct x (y) value',
+ 'is drawn',
+ 'If no `x` (`y`) {array} is provided, a single violin is drawn.',
+ 'That violin position is then positioned with',
+ 'with `name` or with `x0` (`y0`) if provided.'
+ ].join(' ')
+ }
+};
diff --git a/src/traces/violin/layout_attributes.js b/src/traces/violin/layout_attributes.js
new file mode 100644
index 00000000000..4b6fd09596e
--- /dev/null
+++ b/src/traces/violin/layout_attributes.js
@@ -0,0 +1,37 @@
+/**
+* Copyright 2012-2017, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+'use strict';
+
+var boxLayoutAttrs = require('../box/layout_attributes');
+var extendFlat = require('../../lib').extendFlat;
+
+module.exports = {
+ violinmode: extendFlat({}, boxLayoutAttrs.boxmode, {
+ description: [
+ 'Determines how violins at the same location coordinate',
+ 'are displayed on the graph.',
+ 'If *group*, the violins are plotted next to one another',
+ 'centered around the shared location.',
+ 'If *overlay*, the violins are plotted over one another,',
+ 'you might need to set *opacity* to see them multiple violins.'
+ ].join(' ')
+ }),
+ violingap: extendFlat({}, boxLayoutAttrs.boxgap, {
+ description: [
+ 'Sets the gap (in plot fraction) between violins of',
+ 'adjacent location coordinates.'
+ ].join(' ')
+ }),
+ violingroupgap: extendFlat({}, boxLayoutAttrs.boxgroupgap, {
+ description: [
+ 'Sets the gap (in plot fraction) between violins of',
+ 'the same location coordinate.'
+ ].join(' ')
+ })
+};
diff --git a/src/traces/violin/layout_defaults.js b/src/traces/violin/layout_defaults.js
new file mode 100644
index 00000000000..28352b7ec95
--- /dev/null
+++ b/src/traces/violin/layout_defaults.js
@@ -0,0 +1,20 @@
+/**
+* Copyright 2012-2017, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+'use strict';
+
+var Lib = require('../../lib');
+var layoutAttributes = require('./layout_attributes');
+var boxLayoutDefaults = require('../box/layout_defaults');
+
+module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
+ function coerce(attr, dflt) {
+ return Lib.coerce(layoutIn, layoutOut, layoutAttributes, attr, dflt);
+ }
+ boxLayoutDefaults._supply(layoutIn, layoutOut, fullData, coerce, 'violin');
+};
diff --git a/src/traces/violin/plot.js b/src/traces/violin/plot.js
new file mode 100644
index 00000000000..afec439dda0
--- /dev/null
+++ b/src/traces/violin/plot.js
@@ -0,0 +1,202 @@
+/**
+* Copyright 2012-2017, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+'use strict';
+
+var d3 = require('d3');
+var Lib = require('../../lib');
+var Drawing = require('../../components/drawing');
+var boxPlot = require('../box/plot');
+var linePoints = require('../scatter/line_points');
+var helpers = require('./helpers');
+
+module.exports = function plot(gd, plotinfo, cd) {
+ var fullLayout = gd._fullLayout;
+ var xa = plotinfo.xaxis;
+ var ya = plotinfo.yaxis;
+
+ function makePath(pts) {
+ var segments = linePoints(pts, {
+ xaxis: xa,
+ yaxis: ya,
+ connectGaps: true,
+ baseTolerance: 0.75,
+ shape: 'spline',
+ simplify: true
+ });
+ return Drawing.smoothopen(segments[0], 1);
+ }
+
+ var traces = plotinfo.plot.select('.violinlayer')
+ .selectAll('g.trace.violins')
+ .data(cd)
+ .enter().append('g')
+ .attr('class', 'trace violins');
+
+ traces.each(function(d) {
+ var cd0 = d[0];
+ var t = cd0.t;
+ var trace = cd0.trace;
+ var sel = cd0.node3 = d3.select(this);
+ var numViolins = fullLayout._numViolins;
+ var group = (fullLayout.violinmode === 'group' && numViolins > 1);
+ // violin max half width
+ var bdPos = t.bdPos = t.dPos * (1 - fullLayout.violingap) * (1 - fullLayout.violingroupgap) / (group ? numViolins : 1);
+ // violin center offset
+ var bPos = t.bPos = group ? 2 * t.dPos * (-0.5 + (t.num + 0.5) / numViolins) * (1 - fullLayout.violingap) : 0;
+
+ if(trace.visible !== true || t.empty) {
+ d3.select(this).remove();
+ return;
+ }
+
+ var valAxis = plotinfo[t.valLetter + 'axis'];
+ var posAxis = plotinfo[t.posLetter + 'axis'];
+ var hasBothSides = trace.side === 'both';
+ var hasPositiveSide = hasBothSides || trace.side === 'positive';
+ var hasNegativeSide = hasBothSides || trace.side === 'negative';
+ var hasBox = trace.box && trace.box.visible;
+ var hasMeanLine = trace.meanline && trace.meanline.visible;
+ var groupStats = fullLayout._violinScaleGroupStats[trace.scalegroup];
+
+ sel.selectAll('path.violin')
+ .data(Lib.identity)
+ .enter().append('path')
+ .style('vector-effect', 'non-scaling-stroke')
+ .attr('class', 'violin')
+ .each(function(d) {
+ var pathSel = d3.select(this);
+ var density = d.density;
+ var len = density.length;
+ var posCenter = d.pos + bPos;
+ var posCenterPx = posAxis.c2p(posCenter);
+ var scale;
+
+ switch(trace.scalemode) {
+ case 'width':
+ scale = groupStats.maxWidth / bdPos;
+ break;
+ case 'count':
+ scale = (groupStats.maxWidth / bdPos) * (groupStats.maxCount / d.pts.length);
+ break;
+ }
+
+ var pathPos, pathNeg, path;
+ var i, k, pts, pt;
+
+ if(hasPositiveSide) {
+ pts = new Array(len);
+ for(i = 0; i < len; i++) {
+ pt = pts[i] = {};
+ pt[t.posLetter] = posCenter + (density[i].v / scale);
+ pt[t.valLetter] = density[i].t;
+ }
+ pathPos = makePath(pts);
+ }
+
+ if(hasNegativeSide) {
+ pts = new Array(len);
+ for(k = 0, i = len - 1; k < len; k++, i--) {
+ pt = pts[k] = {};
+ pt[t.posLetter] = posCenter - (density[i].v / scale);
+ pt[t.valLetter] = density[i].t;
+ }
+ pathNeg = makePath(pts);
+ }
+
+ if(hasBothSides) {
+ path = pathPos + 'L' + pathNeg.substr(1) + 'Z';
+ }
+ else {
+ var startPt = [posCenterPx, valAxis.c2p(density[0].t)];
+ var endPt = [posCenterPx, valAxis.c2p(density[len - 1].t)];
+
+ if(trace.orientation === 'h') {
+ startPt.reverse();
+ endPt.reverse();
+ }
+
+ if(hasPositiveSide) {
+ path = 'M' + startPt + 'L' + pathPos.substr(1) + 'L' + endPt;
+ } else {
+ path = 'M' + endPt + 'L' + pathNeg.substr(1) + 'L' + startPt;
+ }
+ }
+ pathSel.attr('d', path);
+
+ // save a few things used in getPositionOnKdePath, getKdeValue
+ // on hover and for meanline draw block below
+ d.posCenterPx = posCenterPx;
+ d.posDensityScale = scale * bdPos;
+ d.path = pathSel.node();
+ d.pathLength = d.path.getTotalLength() / (hasBothSides ? 2 : 1);
+ });
+
+ if(hasBox) {
+ var boxWidth = trace.box.width;
+ var boxLineWidth = trace.box.line.width;
+ var bdPosScaled;
+ var bPosPxOffset;
+
+ if(hasBothSides) {
+ bdPosScaled = bdPos * boxWidth;
+ bPosPxOffset = 0;
+ } else if(hasPositiveSide) {
+ bdPosScaled = [0, bdPos * boxWidth / 2];
+ bPosPxOffset = -boxLineWidth;
+ } else {
+ bdPosScaled = [bdPos * boxWidth / 2, 0];
+ bPosPxOffset = boxLineWidth;
+ }
+
+ // do not draw whiskers on inner boxes
+ trace.whiskerwidth = 0;
+
+ boxPlot.plotBoxAndWhiskers(sel, {pos: posAxis, val: valAxis}, trace, {
+ bPos: bPos,
+ bdPos: bdPosScaled,
+ bPosPxOffset: bPosPxOffset
+ });
+
+ // if both box and meanline are visible, show mean line inside box
+ if(hasMeanLine) {
+ boxPlot.plotBoxMean(sel, {pos: posAxis, val: valAxis}, trace, {
+ bPos: bPos,
+ bdPos: bdPosScaled,
+ bPosPxOffset: bPosPxOffset
+ });
+ }
+ }
+ else {
+ if(hasMeanLine) {
+ sel.selectAll('path.mean')
+ .data(Lib.identity)
+ .enter().append('path')
+ .attr('class', 'mean')
+ .style({
+ fill: 'none',
+ 'vector-effect': 'non-scaling-stroke'
+ })
+ .each(function(d) {
+ var v = valAxis.c2p(d.mean, true);
+ var p = helpers.getPositionOnKdePath(d, trace, v);
+
+ d3.select(this).attr('d',
+ trace.orientation === 'h' ?
+ 'M' + v + ',' + p[0] + 'V' + p[1] :
+ 'M' + p[0] + ',' + v + 'H' + p[1]
+ );
+ });
+ }
+ }
+
+ if(trace.points) {
+ boxPlot.plotPoints(sel, {x: xa, y: ya}, trace, t);
+ }
+ });
+};
diff --git a/src/traces/violin/set_positions.js b/src/traces/violin/set_positions.js
new file mode 100644
index 00000000000..b18376d485a
--- /dev/null
+++ b/src/traces/violin/set_positions.js
@@ -0,0 +1,48 @@
+/**
+* Copyright 2012-2017, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+'use strict';
+
+var setPositionOffset = require('../box/set_positions').setPositionOffset;
+var orientations = ['v', 'h'];
+
+module.exports = function setPositions(gd, plotinfo) {
+ var calcdata = gd.calcdata;
+ var xa = plotinfo.xaxis;
+ var ya = plotinfo.yaxis;
+
+ for(var i = 0; i < orientations.length; i++) {
+ var orientation = orientations[i];
+ var posAxis = orientation === 'h' ? ya : xa;
+ var violinList = [];
+ var minPad = 0;
+ var maxPad = 0;
+
+ for(var j = 0; j < calcdata.length; j++) {
+ var cd = calcdata[j];
+ var t = cd[0].t;
+ var trace = cd[0].trace;
+
+ if(trace.visible === true && trace.type === 'violin' &&
+ !t.empty &&
+ trace.orientation === orientation &&
+ trace.xaxis === xa._id &&
+ trace.yaxis === ya._id
+ ) {
+ violinList.push(j);
+
+ if(trace.points !== false) {
+ minPad = Math.max(minPad, trace.jitter - trace.pointpos - 1);
+ maxPad = Math.max(maxPad, trace.jitter + trace.pointpos - 1);
+ }
+ }
+ }
+
+ setPositionOffset('violin', gd, violinList, posAxis, [minPad, maxPad]);
+ }
+};
diff --git a/src/traces/violin/style.js b/src/traces/violin/style.js
new file mode 100644
index 00000000000..a3f0c2d8db0
--- /dev/null
+++ b/src/traces/violin/style.js
@@ -0,0 +1,47 @@
+/**
+* Copyright 2012-2017, Plotly, Inc.
+* All rights reserved.
+*
+* This source code is licensed under the MIT license found in the
+* LICENSE file in the root directory of this source tree.
+*/
+
+'use strict';
+
+var d3 = require('d3');
+var Drawing = require('../../components/drawing');
+var Color = require('../../components/color');
+
+module.exports = function style(gd) {
+ var traces = d3.select(gd).selectAll('g.trace.violins');
+
+ traces.style('opacity', function(d) { return d[0].trace.opacity; })
+ .each(function(d) {
+ var trace = d[0].trace;
+ var sel = d3.select(this);
+ var box = trace.box || {};
+ var boxLine = box.line || {};
+ var meanline = trace.meanline || {};
+ var meanLineWidth = meanline.width;
+
+ sel.selectAll('path.violin')
+ .style('stroke-width', trace.line.width + 'px')
+ .call(Color.stroke, trace.line.color)
+ .call(Color.fill, trace.fillcolor);
+
+ sel.selectAll('path.box')
+ .style('stroke-width', boxLine.width + 'px')
+ .call(Color.stroke, boxLine.color)
+ .call(Color.fill, box.fillcolor);
+
+ sel.selectAll('g.points path')
+ .call(Drawing.pointStyle, trace, gd);
+
+ sel.selectAll('path.mean')
+ .style({
+ 'stroke-width': meanLineWidth + 'px',
+ 'stroke-dasharray': (2 * meanLineWidth) + 'px,' + meanLineWidth + 'px'
+ })
+ .call(Color.stroke, meanline.color);
+ });
+};
diff --git a/test/image/baselines/violin_box_overlay.png b/test/image/baselines/violin_box_overlay.png
new file mode 100644
index 00000000000..bf60201ef43
Binary files /dev/null and b/test/image/baselines/violin_box_overlay.png differ
diff --git a/test/image/baselines/violin_grouped.png b/test/image/baselines/violin_grouped.png
new file mode 100644
index 00000000000..30de14adb18
Binary files /dev/null and b/test/image/baselines/violin_grouped.png differ
diff --git a/test/image/baselines/violin_non-linear.png b/test/image/baselines/violin_non-linear.png
new file mode 100644
index 00000000000..3609f3890e7
Binary files /dev/null and b/test/image/baselines/violin_non-linear.png differ
diff --git a/test/image/baselines/violin_old-faithful.png b/test/image/baselines/violin_old-faithful.png
new file mode 100644
index 00000000000..26d9b81943a
Binary files /dev/null and b/test/image/baselines/violin_old-faithful.png differ
diff --git a/test/image/baselines/violin_side-by-side.png b/test/image/baselines/violin_side-by-side.png
new file mode 100644
index 00000000000..591ff342f6c
Binary files /dev/null and b/test/image/baselines/violin_side-by-side.png differ
diff --git a/test/image/baselines/violin_style.png b/test/image/baselines/violin_style.png
new file mode 100644
index 00000000000..60e47221c55
Binary files /dev/null and b/test/image/baselines/violin_style.png differ
diff --git a/test/image/mocks/violin_box_overlay.json b/test/image/mocks/violin_box_overlay.json
new file mode 100644
index 00000000000..b12add9e702
--- /dev/null
+++ b/test/image/mocks/violin_box_overlay.json
@@ -0,0 +1,21 @@
+{
+ "data": [{
+ "type": "violin",
+ "x0": "sample 1",
+ "name": "violin",
+ "points": "all",
+ "y": [79, 54, 74, 62, 85, 55, 88, 85, 51, 85, 54, 84, 78, 47, 83, 52, 62, 84, 52, 79, 51, 47, 78, 69, 74, 83, 55, 76, 78, 79, 73, 77, 66, 80, 74, 52, 48, 80, 59, 90, 80, 58, 84, 58, 73, 83, 64, 53, 82, 59, 75, 90, 54, 80, 54, 83, 71, 64, 77, 81, 59, 84, 48, 82, 60, 92, 78, 78, 65, 73, 82, 56, 79, 71, 62, 76, 60, 78, 76, 83, 75, 82, 70, 65, 73, 88, 76, 80, 48, 86, 60, 90, 50, 78, 63, 72, 84, 75, 51, 82, 62, 88, 49, 83, 81, 47, 84, 52, 86, 81, 75, 59, 89, 79, 59, 81, 50, 85, 59, 87, 53, 69, 77, 56, 88, 81, 45, 82, 55, 90, 45, 83, 56, 89, 46, 82, 51, 86, 53, 79, 81, 60, 82, 77, 76, 59, 80, 49, 96, 53, 77, 77, 65, 81, 71, 70, 81, 93, 53, 89, 45, 86, 58, 78, 66, 76, 63, 88, 52, 93, 49, 57, 77, 68, 81, 81, 73, 50, 85, 74, 55, 77, 83, 83, 51, 78, 84, 46, 83, 55, 81, 57, 76, 84, 77, 81, 87, 77, 51, 78, 60, 82, 91, 53, 78, 46, 77, 84, 49, 83, 71, 80, 49, 75, 64, 76, 53, 94, 55, 76, 50, 82, 54, 75, 78, 79, 78, 78, 70, 79, 70, 54, 86, 50, 90, 54, 54, 77, 79, 64, 75, 47, 86, 63, 85, 82, 57, 82, 67, 74, 54, 83, 73, 73, 88, 80, 71, 83, 56, 79, 78, 84, 58, 83, 43, 60, 75, 81, 46, 90, 46, 74 ]
+ }, {
+ "type": "box",
+ "x0": "sample 1",
+ "name": "box",
+ "boxpoints": false,
+ "y": [79, 54, 74, 62, 85, 55, 88, 85, 51, 85, 54, 84, 78, 47, 83, 52, 62, 84, 52, 79, 51, 47, 78, 69, 74, 83, 55, 76, 78, 79, 73, 77, 66, 80, 74, 52, 48, 80, 59, 90, 80, 58, 84, 58, 73, 83, 64, 53, 82, 59, 75, 90, 54, 80, 54, 83, 71, 64, 77, 81, 59, 84, 48, 82, 60, 92, 78, 78, 65, 73, 82, 56, 79, 71, 62, 76, 60, 78, 76, 83, 75, 82, 70, 65, 73, 88, 76, 80, 48, 86, 60, 90, 50, 78, 63, 72, 84, 75, 51, 82, 62, 88, 49, 83, 81, 47, 84, 52, 86, 81, 75, 59, 89, 79, 59, 81, 50, 85, 59, 87, 53, 69, 77, 56, 88, 81, 45, 82, 55, 90, 45, 83, 56, 89, 46, 82, 51, 86, 53, 79, 81, 60, 82, 77, 76, 59, 80, 49, 96, 53, 77, 77, 65, 81, 71, 70, 81, 93, 53, 89, 45, 86, 58, 78, 66, 76, 63, 88, 52, 93, 49, 57, 77, 68, 81, 81, 73, 50, 85, 74, 55, 77, 83, 83, 51, 78, 84, 46, 83, 55, 81, 57, 76, 84, 77, 81, 87, 77, 51, 78, 60, 82, 91, 53, 78, 46, 77, 84, 49, 83, 71, 80, 49, 75, 64, 76, 53, 94, 55, 76, 50, 82, 54, 75, 78, 79, 78, 78, 70, 79, 70, 54, 86, 50, 90, 54, 54, 77, 79, 64, 75, 47, 86, 63, 85, 82, 57, 82, 67, 74, 54, 83, 73, 73, 88, 80, 71, 83, 56, 79, 78, 84, 58, 83, 43, 60, 75, 81, 46, 90, 46, 74 ]
+ }],
+ "layout": {
+ "legend": {
+ "x": 0.95,
+ "xanchor": "right"
+ }
+ }
+}
diff --git a/test/image/mocks/violin_grouped.json b/test/image/mocks/violin_grouped.json
new file mode 100644
index 00000000000..3662401dc38
--- /dev/null
+++ b/test/image/mocks/violin_grouped.json
@@ -0,0 +1,76 @@
+{
+ "data": [{
+ "type": "violin",
+ "name": "kale",
+ "y": [
+ 0.2, 0.2, 0.6, 1, 0.5, 0.4,
+ 0.2, 0.7, 0.9, 0.1, 0.5, 0.3
+ ],
+ "x": [
+ "day 1", "day 1", "day 1", "day 1", "day 1", "day 1",
+ "day 2", "day 2", "day 2", "day 2", "day 2", "day 2"
+ ],
+ "marker": {
+ "symbol": "line-ew",
+ "color": "#3D9970",
+ "line": {
+ "color": "#3D9970",
+ "width": 2
+ }
+ },
+ "points": "all",
+ "jitter": 0,
+ "span": [0, null]
+ }, {
+ "type": "violin",
+ "name": "radishes",
+ "y": [
+ 0.6, 0.7, 0.3, 0.6, 0, 0.5,
+ 0.7, 0.9, 0.5, 0.8, 0.7, 0.2
+ ],
+ "x": [
+ "day 1", "day 1", "day 1", "day 1", "day 1", "day 1",
+ "day 2", "day 2", "day 2", "day 2", "day 2", "day 2"
+ ],
+ "marker": {
+ "symbol": "line-ew",
+ "color": "#FF4136",
+ "line": {
+ "color": "#FF4136",
+ "width": 2
+ }
+ },
+ "points": "all",
+ "jitter": 0,
+ "span": [0, null]
+ }, {
+ "type": "violin",
+ "name": "carrots",
+ "y": [
+ 0.1, 0.3, 0.1, 0.9, 0.6, 0.6,
+ 0.9, 1, 0.3, 0.6, 0.8, 0.5
+ ],
+ "x": [
+ "day 1", "day 1", "day 1", "day 1", "day 1", "day 1",
+ "day 2", "day 2", "day 2", "day 2", "day 2", "day 2"
+ ],
+ "marker": {
+ "symbol": "line-ew",
+ "color": "#FF851B",
+ "line": {
+ "color": "#FF851B",
+ "width": 2
+ }
+ },
+ "points": "all",
+ "jitter": 0,
+ "span": [0, null]
+ }],
+ "layout": {
+ "yaxis": {
+ "zeroline": false,
+ "title": "normalized moisture"
+ },
+ "violinmode": "group"
+ }
+}
diff --git a/test/image/mocks/violin_non-linear.json b/test/image/mocks/violin_non-linear.json
new file mode 100644
index 00000000000..e299a68dc83
--- /dev/null
+++ b/test/image/mocks/violin_non-linear.json
@@ -0,0 +1,58 @@
+{
+ "data": [{
+ "type": "violin",
+ "x": [
+ "1798-01-01",
+ "1798-04-04",
+ "1798-05-05",
+ "1798-05-05",
+ "1798-07-05",
+ "1798-07-22",
+ "1799-01-01"
+ ],
+ "orientation": "h",
+ "xcalendar": "discworld",
+ "name": "discworld dates",
+ "box": {"visible": true},
+ "xaxis": "x",
+ "yaxis": "y"
+ }, {
+ "type": "violin",
+ "x": [
+ "A",
+ "B",
+ "C",
+ "C",
+ "C",
+ "D",
+ "G"
+ ],
+ "orientation": "h",
+ "name": "categories",
+ "box": {"visible": true},
+ "xaxis": "x2",
+ "yaxis": "y2"
+
+ }],
+ "layout": {
+ "xaxis": {
+ "anchor": "y"
+ },
+ "xaxis2": {
+ "anchor": "y2",
+ "categoryarray": ["A", "B", "C", "D", "E", "F", "G"]
+ },
+ "yaxis": {
+ "anchor": "x",
+ "domain": [0, 0.48]
+ },
+ "yaxis2": {
+ "anchor": "x2",
+ "domain": [0.52, 1]
+ },
+ "margin": {
+ "l": 100
+ },
+ "showlegend": false
+ }
+}
diff --git a/test/image/mocks/violin_old-faithful.json b/test/image/mocks/violin_old-faithful.json
new file mode 100644
index 00000000000..9c7239b5a44
--- /dev/null
+++ b/test/image/mocks/violin_old-faithful.json
@@ -0,0 +1,9 @@
+{
+ "data": [{
+ "type": "violin",
+ "points": "all",
+ "name": "Old Faithful",
+ "meanline": {"visible": true},
+ "y": [79, 54, 74, 62, 85, 55, 88, 85, 51, 85, 54, 84, 78, 47, 83, 52, 62, 84, 52, 79, 51, 47, 78, 69, 74, 83, 55, 76, 78, 79, 73, 77, 66, 80, 74, 52, 48, 80, 59, 90, 80, 58, 84, 58, 73, 83, 64, 53, 82, 59, 75, 90, 54, 80, 54, 83, 71, 64, 77, 81, 59, 84, 48, 82, 60, 92, 78, 78, 65, 73, 82, 56, 79, 71, 62, 76, 60, 78, 76, 83, 75, 82, 70, 65, 73, 88, 76, 80, 48, 86, 60, 90, 50, 78, 63, 72, 84, 75, 51, 82, 62, 88, 49, 83, 81, 47, 84, 52, 86, 81, 75, 59, 89, 79, 59, 81, 50, 85, 59, 87, 53, 69, 77, 56, 88, 81, 45, 82, 55, 90, 45, 83, 56, 89, 46, 82, 51, 86, 53, 79, 81, 60, 82, 77, 76, 59, 80, 49, 96, 53, 77, 77, 65, 81, 71, 70, 81, 93, 53, 89, 45, 86, 58, 78, 66, 76, 63, 88, 52, 93, 49, 57, 77, 68, 81, 81, 73, 50, 85, 74, 55, 77, 83, 83, 51, 78, 84, 46, 83, 55, 81, 57, 76, 84, 77, 81, 87, 77, 51, 78, 60, 82, 91, 53, 78, 46, 77, 84, 49, 83, 71, 80, 49, 75, 64, 76, 53, 94, 55, 76, 50, 82, 54, 75, 78, 79, 78, 78, 70, 79, 70, 54, 86, 50, 90, 54, 54, 77, 79, 64, 75, 47, 86, 63, 85, 82, 57, 82, 67, 74, 54, 83, 73, 73, 88, 80, 71, 83, 56, 79, 78, 84, 58, 83, 43, 60, 75, 81, 46, 90, 46, 74 ]
+ }]
+}
diff --git a/test/image/mocks/violin_side-by-side.json b/test/image/mocks/violin_side-by-side.json
new file mode 100644
index 00000000000..140170adb0a
--- /dev/null
+++ b/test/image/mocks/violin_side-by-side.json
@@ -0,0 +1,559 @@
+{
+ "layout": {
+ "hovermode": "closest",
+ "width": 400,
+ "yaxis": {
+ "showgrid": true
+ },
+ "title": "Total bill distribution
scaled by number of bills per gender",
+ "legend": {
+ "tracegroupgap": 0
+ },
+ "violingap": 0,
+ "violingroupgap": 0,
+ "violinmode": "overlay",
+ "height": 700
+ },
+ "data": [
+ {
+ "text": "sample length: 32",
+ "hoveron": "points+kde",
+ "meanline": {
+ "visible": true
+ },
+ "legendgroup": "F",
+ "scalegroup": "F",
+ "points": "all",
+ "pointpos": 1,
+ "box": {
+ "visible": true
+ },
+ "jitter": 0,
+ "scalemode": "count",
+ "marker": {
+ "line": {
+ "width": 2,
+ "color": "#bebada"
+ },
+ "symbol": "line-ns"
+ },
+ "showlegend": false,
+ "side": "positive",
+ "type": "violin",
+ "name": "F",
+ "span": [
+ 0
+ ],
+ "line": {
+ "color": "#bebada"
+ },
+ "y0": "Thursday",
+ "x": [
+ 10.07,
+ 34.83,
+ 10.65,
+ 12.43,
+ 24.08,
+ 13.42,
+ 12.48,
+ 29.8,
+ 14.52,
+ 11.38,
+ 20.27,
+ 11.17,
+ 12.26,
+ 18.26,
+ 8.51,
+ 10.33,
+ 14.15,
+ 13.16,
+ 17.47,
+ 27.05,
+ 16.43,
+ 8.35,
+ 18.64,
+ 11.87,
+ 19.81,
+ 43.11,
+ 13.0,
+ 12.74,
+ 13.0,
+ 16.4,
+ 16.47,
+ 18.78
+ ],
+ "orientation": "h"
+ },
+ {
+ "text": "sample length: 30",
+ "hoveron": "points+kde",
+ "meanline": {
+ "visible": true
+ },
+ "legendgroup": "M",
+ "scalegroup": "M",
+ "points": "all",
+ "pointpos": -0.6,
+ "box": {
+ "visible": true
+ },
+ "jitter": 0,
+ "scalemode": "count",
+ "marker": {
+ "line": {
+ "width": 2,
+ "color": "#8dd3c7"
+ },
+ "symbol": "line-ns"
+ },
+ "showlegend": false,
+ "side": "negative",
+ "type": "violin",
+ "name": "M",
+ "span": [
+ 0
+ ],
+ "line": {
+ "color": "#8dd3c7"
+ },
+ "y0": "Thursday",
+ "x": [
+ 27.2,
+ 22.76,
+ 17.29,
+ 19.44,
+ 16.66,
+ 32.68,
+ 15.98,
+ 13.03,
+ 18.28,
+ 24.71,
+ 21.16,
+ 11.69,
+ 14.26,
+ 15.95,
+ 8.52,
+ 22.82,
+ 19.08,
+ 16.0,
+ 34.3,
+ 41.19,
+ 9.78,
+ 7.51,
+ 28.44,
+ 15.48,
+ 16.58,
+ 7.56,
+ 10.34,
+ 13.51,
+ 18.71,
+ 20.53
+ ],
+ "orientation": "h"
+ },
+ {
+ "text": "sample length: 9",
+ "hoveron": "points+kde",
+ "meanline": {
+ "visible": true
+ },
+ "legendgroup": "F",
+ "scalegroup": "F",
+ "points": "all",
+ "pointpos": 0.4,
+ "box": {
+ "visible": true
+ },
+ "jitter": 0,
+ "scalemode": "count",
+ "marker": {
+ "line": {
+ "width": 2,
+ "color": "#bebada"
+ },
+ "symbol": "line-ns"
+ },
+ "showlegend": false,
+ "side": "positive",
+ "type": "violin",
+ "name": "F",
+ "span": [
+ 0
+ ],
+ "line": {
+ "color": "#bebada"
+ },
+ "y0": "Friday",
+ "x": [
+ 5.75,
+ 16.32,
+ 22.75,
+ 11.35,
+ 15.38,
+ 13.42,
+ 15.98,
+ 16.27,
+ 10.09
+ ],
+ "orientation": "h"
+ },
+ {
+ "text": "sample length: 10",
+ "hoveron": "points+kde",
+ "meanline": {
+ "visible": true
+ },
+ "legendgroup": "M",
+ "scalegroup": "M",
+ "points": "all",
+ "pointpos": -0.3,
+ "box": {
+ "visible": true
+ },
+ "jitter": 0,
+ "scalemode": "count",
+ "marker": {
+ "line": {
+ "width": 2,
+ "color": "#8dd3c7"
+ },
+ "symbol": "line-ns"
+ },
+ "showlegend": false,
+ "side": "negative",
+ "type": "violin",
+ "name": "M",
+ "span": [
+ 0
+ ],
+ "line": {
+ "color": "#8dd3c7"
+ },
+ "y0": "Friday",
+ "x": [
+ 28.97,
+ 22.49,
+ 40.17,
+ 27.28,
+ 12.03,
+ 21.01,
+ 12.46,
+ 12.16,
+ 8.58,
+ 13.42
+ ],
+ "orientation": "h"
+ },
+ {
+ "text": "sample length: 28",
+ "hoveron": "points+kde",
+ "meanline": {
+ "visible": true
+ },
+ "legendgroup": "F",
+ "scalegroup": "F",
+ "points": "all",
+ "pointpos": 0.55,
+ "box": {
+ "visible": true
+ },
+ "jitter": 0,
+ "scalemode": "count",
+ "marker": {
+ "line": {
+ "width": 2,
+ "color": "#bebada"
+ },
+ "symbol": "line-ns"
+ },
+ "showlegend": true,
+ "side": "positive",
+ "type": "violin",
+ "name": "F",
+ "span": [
+ 0
+ ],
+ "line": {
+ "color": "#bebada"
+ },
+ "y0": "Saturday",
+ "x": [
+ 20.29,
+ 15.77,
+ 19.65,
+ 15.06,
+ 20.69,
+ 16.93,
+ 26.41,
+ 16.45,
+ 3.07,
+ 17.07,
+ 26.86,
+ 25.28,
+ 14.73,
+ 44.3,
+ 22.42,
+ 20.92,
+ 14.31,
+ 7.25,
+ 10.59,
+ 10.63,
+ 12.76,
+ 13.27,
+ 28.17,
+ 12.9,
+ 30.14,
+ 22.12,
+ 35.83,
+ 27.18
+ ],
+ "orientation": "h"
+ },
+ {
+ "text": "sample length: 59",
+ "hoveron": "points+kde",
+ "meanline": {
+ "visible": true
+ },
+ "legendgroup": "M",
+ "scalegroup": "M",
+ "points": "all",
+ "pointpos": -1.1,
+ "box": {
+ "visible": true
+ },
+ "jitter": 0,
+ "scalemode": "count",
+ "marker": {
+ "line": {
+ "width": 2,
+ "color": "#8dd3c7"
+ },
+ "symbol": "line-ns"
+ },
+ "showlegend": true,
+ "side": "negative",
+ "type": "violin",
+ "name": "M",
+ "span": [
+ 0
+ ],
+ "line": {
+ "color": "#8dd3c7"
+ },
+ "y0": "Saturday",
+ "x": [
+ 20.65,
+ 17.92,
+ 39.42,
+ 19.82,
+ 17.81,
+ 13.37,
+ 12.69,
+ 21.7,
+ 9.55,
+ 18.35,
+ 17.78,
+ 24.06,
+ 16.31,
+ 18.69,
+ 31.27,
+ 16.04,
+ 38.01,
+ 11.24,
+ 48.27,
+ 20.29,
+ 13.81,
+ 11.02,
+ 18.29,
+ 17.59,
+ 20.08,
+ 20.23,
+ 15.01,
+ 12.02,
+ 10.51,
+ 17.92,
+ 15.36,
+ 20.49,
+ 25.21,
+ 18.24,
+ 14.0,
+ 50.81,
+ 15.81,
+ 26.59,
+ 38.73,
+ 24.27,
+ 30.06,
+ 25.89,
+ 48.33,
+ 28.15,
+ 11.59,
+ 7.74,
+ 20.45,
+ 13.28,
+ 24.01,
+ 15.69,
+ 11.61,
+ 10.77,
+ 15.53,
+ 10.07,
+ 12.6,
+ 32.83,
+ 29.03,
+ 22.67,
+ 17.82
+ ],
+ "orientation": "h"
+ },
+ {
+ "text": "sample length: 18",
+ "hoveron": "points+kde",
+ "meanline": {
+ "visible": true
+ },
+ "legendgroup": "F",
+ "scalegroup": "F",
+ "points": "all",
+ "pointpos": 0.45,
+ "box": {
+ "visible": true
+ },
+ "jitter": 0,
+ "scalemode": "count",
+ "marker": {
+ "line": {
+ "width": 2,
+ "color": "#bebada"
+ },
+ "symbol": "line-ns"
+ },
+ "showlegend": false,
+ "side": "positive",
+ "type": "violin",
+ "name": "F",
+ "span": [
+ 0
+ ],
+ "line": {
+ "color": "#bebada"
+ },
+ "y0": "Sunday",
+ "x": [
+ 16.99,
+ 24.59,
+ 35.26,
+ 14.83,
+ 10.33,
+ 16.97,
+ 10.29,
+ 34.81,
+ 25.71,
+ 17.31,
+ 29.85,
+ 25.0,
+ 13.39,
+ 16.21,
+ 17.51,
+ 9.6,
+ 20.9,
+ 18.15
+ ],
+ "orientation": "h"
+ },
+ {
+ "text": "sample length: 58",
+ "hoveron": "points+kde",
+ "meanline": {
+ "visible": true
+ },
+ "legendgroup": "M",
+ "scalegroup": "M",
+ "points": "all",
+ "pointpos": -0.9,
+ "box": {
+ "visible": true
+ },
+ "jitter": 0,
+ "scalemode": "count",
+ "marker": {
+ "line": {
+ "width": 2,
+ "color": "#8dd3c7"
+ },
+ "symbol": "line-ns"
+ },
+ "showlegend": false,
+ "side": "negative",
+ "type": "violin",
+ "name": "M",
+ "span": [
+ 0
+ ],
+ "line": {
+ "color": "#8dd3c7"
+ },
+ "y0": "Sunday",
+ "x": [
+ 10.34,
+ 21.01,
+ 23.68,
+ 25.29,
+ 8.77,
+ 26.88,
+ 15.04,
+ 14.78,
+ 10.27,
+ 15.42,
+ 18.43,
+ 21.58,
+ 16.29,
+ 17.46,
+ 13.94,
+ 9.68,
+ 30.4,
+ 18.29,
+ 22.23,
+ 32.4,
+ 28.55,
+ 18.04,
+ 12.54,
+ 9.94,
+ 25.56,
+ 19.49,
+ 38.07,
+ 23.95,
+ 29.93,
+ 14.07,
+ 13.13,
+ 17.26,
+ 24.55,
+ 19.77,
+ 48.17,
+ 16.49,
+ 21.5,
+ 12.66,
+ 13.81,
+ 24.52,
+ 20.76,
+ 31.71,
+ 7.25,
+ 31.85,
+ 16.82,
+ 32.9,
+ 17.89,
+ 14.48,
+ 34.63,
+ 34.65,
+ 23.33,
+ 45.35,
+ 23.17,
+ 40.55,
+ 20.69,
+ 30.46,
+ 23.1,
+ 15.69
+ ],
+ "orientation": "h"
+ }
+ ]
+}
diff --git a/test/image/mocks/violin_style.json b/test/image/mocks/violin_style.json
new file mode 100644
index 00000000000..b7e5462be33
--- /dev/null
+++ b/test/image/mocks/violin_style.json
@@ -0,0 +1,507 @@
+{
+ "layout": {
+ "annotations": [
+ {
+ "yref": "paper",
+ "text": "Orbital Period Estimations",
+ "xref": "paper",
+ "showarrow": false,
+ "font": {
+ "size": 20
+ }
+ }
+ ],
+ "paper_bgcolor": "#d3d3d3",
+ "yaxis": {
+ "showline": false,
+ "showticklabels": false,
+ "range": [
+ -5,
+ 550
+ ],
+ "zeroline": false,
+ "visible": false
+ },
+ "showlegend": false,
+ "violingap": 0,
+ "xaxis": {
+ "side": "top"
+ },
+ "plot_bgcolor": "#d3d3d3",
+ "margin": {
+ "l": 0,
+ "r": 0,
+ "b": 0,
+ "t": 20
+ }
+ },
+ "data": [
+ {
+ "bandwidth": 5,
+ "points": false,
+ "y": [
+ 269.3,
+ 874.7739999999999,
+ 763.0,
+ 326.03,
+ 516.22,
+ 185.84,
+ 798.5,
+ 993.3,
+ 452.8,
+ 883.0,
+ 335.1,
+ 479.1,
+ 4.230785,
+ 14.651,
+ 44.38,
+ 0.73654,
+ 261.2,
+ 4.215,
+ 38.021,
+ 123.01,
+ 116.6884,
+ 691.9,
+ 952.7,
+ 181.4,
+ 380.8,
+ 3.2357,
+ 417.9,
+ 594.9,
+ 428.5,
+ 903.3,
+ 136.75,
+ 530.32,
+ 277.02,
+ 187.83,
+ 39.845,
+ 3.3135,
+ 305.5,
+ 4.617033,
+ 241.25799999999998,
+ 655.6,
+ 714.3,
+ 3.48777,
+ 5.6,
+ 237.6,
+ 3.8728,
+ 125.94,
+ 268.94,
+ 137.48,
+ 379.63,
+ 621.99,
+ 578.2,
+ 392.6,
+ 3.698,
+ 15.76491,
+ 8.631,
+ 25.6,
+ 603.0,
+ 692.0,
+ 7.3709,
+ 2.64385,
+ 5.36874,
+ 12.9292,
+ 66.8,
+ 3.14942,
+ 598.3,
+ 7.2004,
+ 28.14,
+ 91.61,
+ 62.24,
+ 39.025999999999996,
+ 256.2,
+ 4.6938,
+ 3.6,
+ 35.37,
+ 61.1166,
+ 30.0881,
+ 1.9377799999999998,
+ 124.26,
+ 133.71,
+ 3.33714,
+ 2.64561,
+ 428.5,
+ 349.7,
+ 5.7727,
+ 13.505,
+ 431.8,
+ 533.0,
+ 3.4442,
+ 311.6,
+ 62.218,
+ 526.62,
+ 829.0,
+ 15.609000000000002,
+ 431.88,
+ 356.0,
+ 360.2,
+ 675.0,
+ 777.0,
+ 792.6,
+ 177.11,
+ 22.09,
+ 615.0,
+ 5.3978,
+ 227.0,
+ 30.052,
+ 192.9,
+ 5.75962,
+ 16.3567,
+ 49.747,
+ 122.72,
+ 602.0,
+ 989.2,
+ 170.455,
+ 711.0,
+ 37.91,
+ 262.709,
+ 471.6,
+ 14.182,
+ 53.832,
+ 19.382,
+ 931.0,
+ 75.523,
+ 17.24,
+ 990.0,
+ 465.1,
+ 359.9,
+ 21.21663,
+ 772.0,
+ 466.2,
+ 11.849,
+ 33.823,
+ 500.0,
+ 18.315,
+ 40.114000000000004,
+ 90.309,
+ 29.15,
+ 85.131,
+ 591.9,
+ 380.85,
+ 22.656,
+ 53.881,
+ 738.459,
+ 528.07,
+ 423.841,
+ 17.991,
+ 385.9,
+ 387.1,
+ 912.0,
+ 466.0,
+ 16.546,
+ 51.284,
+ 274.49,
+ 326.6,
+ 18.179000000000002,
+ 157.54,
+ 388.0,
+ 363.2,
+ 154.46,
+ 843.6,
+ 55.0,
+ 5.6363,
+ 14.025,
+ 33.941,
+ 14.3098,
+ 696.3,
+ 407.15,
+ 4.3123,
+ 9.6184,
+ 20.432000000000002,
+ 34.62,
+ 51.76,
+ 197.8,
+ 264.15,
+ 963.0,
+ 1.3283,
+ 18.357,
+ 25.648000000000003,
+ 327.8,
+ 36.96,
+ 472.0,
+ 5.8872,
+ 226.93,
+ 342.85,
+ 890.76,
+ 43.6,
+ 3.0239999999999996,
+ 4.0845,
+ 430.0,
+ 700.0,
+ 4.9437,
+ 14.07,
+ 95.415,
+ 118.96,
+ 303.0,
+ 201.83,
+ 607.06,
+ 2.817822,
+ 589.64,
+ 358.0,
+ 572.4,
+ 152.6,
+ 480.5,
+ 6.276,
+ 8.667,
+ 31.56,
+ 197.0,
+ 851.8,
+ 2.54858,
+ 188.9,
+ 379.1,
+ 51.645,
+ 346.6,
+ 3.51,
+ 418.2,
+ 3.971,
+ 5.7361,
+ 111.4357,
+ 184.02,
+ 441.47,
+ 220.078,
+ 705.0,
+ 2.985625,
+ 788.0,
+ 58.43,
+ 2.1375,
+ 3.4160000000000004,
+ 256.78,
+ 49.77,
+ 325.81,
+ 143.58,
+ 13.186,
+ 46.025,
+ 507.0,
+ 361.1,
+ 498.9,
+ 647.3,
+ 8.1256,
+ 103.49,
+ 9.494,
+ 436.9,
+ 439.3,
+ 17.054000000000002,
+ 868.0,
+ 157.57,
+ 383.7,
+ 70.46,
+ 20.8133,
+ 4.113775,
+ 127.58,
+ 520.0,
+ 122.1,
+ 778.1,
+ 6.495,
+ 47.84,
+ 5.8881,
+ 55.806000000000004,
+ 199.505,
+ 48.056000000000004,
+ 10.8985,
+ 443.4,
+ 395.8,
+ 68.27,
+ 7.8543,
+ 30.93,
+ 5.24,
+ 835.477,
+ 324.0,
+ 263.3,
+ 937.7,
+ 83.88799999999999,
+ 493.7,
+ 670.0,
+ 25.826999999999998,
+ 6.1335,
+ 63.33,
+ 344.95,
+ 559.4,
+ 4.1547,
+ 9.6737,
+ 948.12,
+ 454.2,
+ 923.8,
+ 10.7085,
+ 883.0,
+ 974.0,
+ 3.27,
+ 258.19,
+ 12.083,
+ 59.519,
+ 459.26,
+ 464.3,
+ 11.577,
+ 27.581999999999997,
+ 106.72,
+ 330.0,
+ 653.22,
+ 386.3,
+ 176.3,
+ 103.95,
+ 44.236000000000004,
+ 528.4,
+ 331.5,
+ 2.8758911,
+ 4.072,
+ 2.391,
+ 689.0,
+ 499.4,
+ 18.596,
+ 163.9,
+ 408.6,
+ 194.3,
+ 391.9,
+ 131.05,
+ 842.0,
+ 4.6455,
+ 359.5546,
+ 104.84,
+ 521.0,
+ 12.62,
+ 248.4,
+ 352.3,
+ 643.25,
+ 9.6386,
+ 310.55,
+ 8.428198,
+ 75.29,
+ 282.4,
+ 606.4,
+ 420.77,
+ 58.11289,
+ 6.403,
+ 225.62,
+ 538.0,
+ 323.6,
+ 297.3,
+ 406.6,
+ 110.9,
+ 71.484,
+ 14.475999999999999,
+ 3.0925,
+ 396.03,
+ 479.0,
+ 663.0,
+ 9.3743,
+ 962.0,
+ 956.0,
+ 634.23,
+ 6.837999999999999,
+ 986.0,
+ 3.097,
+ 456.46,
+ 14.275,
+ 2.21857578,
+ 17.1,
+ 24.348000000000003,
+ 74.72,
+ 525.8,
+ 351.5,
+ 18.201629999999998,
+ 613.8,
+ 825.0,
+ 255.87,
+ 34.873000000000005,
+ 279.8,
+ 610.0,
+ 161.97,
+ 123.0,
+ 875.5,
+ 3.52474859,
+ 442.1,
+ 354.8,
+ 2.245715,
+ 373.3,
+ 951.0,
+ 7.2825,
+ 10.866,
+ 191.99,
+ 3.93,
+ 567.0,
+ 118.45,
+ 7.126816000000001,
+ 225.7,
+ 3.8335,
+ 672.1,
+ 456.1,
+ 572.38,
+ 26.73,
+ 141.6,
+ 192.0,
+ 501.75,
+ 745.7,
+ 8.7836,
+ 3.3689999999999998,
+ 345.72,
+ 57.0,
+ 16.2,
+ 6.6738550000000005,
+ 147.73,
+ 952.0,
+ 41.397,
+ 8.1352,
+ 32.03,
+ 431.7,
+ 124.6,
+ 511.098,
+ 111.7,
+ 5.0505,
+ 311.288,
+ 123.0,
+ 982.0,
+ 580.0,
+ 820.3,
+ 789.0,
+ 677.8,
+ 6.957999999999999,
+ 5.118,
+ 121.71,
+ 4.4264,
+ 2.1451
+ ],
+ "type": "violin",
+ "name": "Radial Velocity",
+ "span": [
+ 0,
+ null
+ ],
+ "line": {
+ "color": "#67353E"
+ },
+ "box": {
+ "fillcolor": "black",
+ "line": {
+ "color": "black"
+ },
+ "width": 0.01
+ }
+ },
+ {
+ "bandwidth": 5,
+ "points": false,
+ "y": [
+ 25.261999999999997,
+ 66.5419,
+ 98.2114,
+ 0.09070629
+ ],
+ "type": "violin",
+ "name": "Pulsar Timing",
+ "span": [
+ 0,
+ null
+ ],
+ "line": {
+ "color": "#34ABA2"
+ },
+ "box": {
+ "fillcolor": "black",
+ "line": {
+ "color": "black"
+ },
+ "width": 0.01
+ }
+ }
+ ]
+}
diff --git a/test/jasmine/tests/plots_test.js b/test/jasmine/tests/plots_test.js
index d529b7363ed..355a7daf07b 100644
--- a/test/jasmine/tests/plots_test.js
+++ b/test/jasmine/tests/plots_test.js
@@ -435,7 +435,7 @@ describe('Test Plots', function() {
var expectedUndefined = [
'data', 'layout', '_fullData', '_fullLayout', 'calcdata', 'framework',
'empty', 'fid', 'undoqueue', 'undonum', 'autoplay', 'changed',
- '_promises', '_redrawTimer', 'firstscatter', 'numboxes',
+ '_promises', '_redrawTimer', 'firstscatter',
'_transitionData', '_transitioning', '_hmpixcount', '_hmlumcount',
'_dragging', '_dragged', '_hoverdata', '_snapshotInProgress', '_editing',
'_replotPending', '_mouseDownTime', '_legendMouseDownTime'
diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js
index 72f29593e3f..3bf53a8337e 100644
--- a/test/jasmine/tests/select_test.js
+++ b/test/jasmine/tests/select_test.js
@@ -971,6 +971,58 @@ describe('Test select box and lasso per trace:', function() {
.catch(fail)
.then(done);
});
+
+ it('should work for violin traces', function(done) {
+ var assertPoints = makeAssertPoints(['curveNumber', 'y', 'x']);
+ var assertRanges = makeAssertRanges();
+ var assertLassoPoints = makeAssertLassoPoints();
+
+ var fig = Lib.extendDeep({}, require('@mocks/violin_grouped'));
+ fig.layout.dragmode = 'lasso';
+ fig.layout.width = 600;
+ fig.layout.height = 500;
+ addInvisible(fig);
+
+ Plotly.plot(gd, fig)
+ .then(function() {
+ return _run(
+ [[200, 200], [400, 200], [400, 350], [200, 350], [200, 200]],
+ function() {
+ assertPoints([
+ [0, 0.3, 'day 2'], [0, 0.5, 'day 2'], [0, 0.7, 'day 2'], [0, 0.9, 'day 2'],
+ [1, 0.5, 'day 2'], [1, 0.7, 'day 2'], [1, 0.7, 'day 2'], [1, 0.8, 'day 2'],
+ [1, 0.9, 'day 2'],
+ [2, 0.3, 'day 1'], [2, 0.6, 'day 1'], [2, 0.6, 'day 1'], [2, 0.9, 'day 1']
+ ]);
+ assertLassoPoints([
+ ['day 1', 'day 2', 'day 2', 'day 1', 'day 1'],
+ [1.02, 1.02, 0.27, 0.27, 1.02]
+ ]);
+ },
+ null, LASSOEVENTS, 'violin lasso'
+ );
+ })
+ .then(function() {
+ return Plotly.relayout(gd, 'dragmode', 'select');
+ })
+ .then(function() {
+ return _run(
+ [[200, 200], [400, 350]],
+ function() {
+ assertPoints([
+ [0, 0.3, 'day 2'], [0, 0.5, 'day 2'], [0, 0.7, 'day 2'], [0, 0.9, 'day 2'],
+ [1, 0.5, 'day 2'], [1, 0.7, 'day 2'], [1, 0.7, 'day 2'], [1, 0.8, 'day 2'],
+ [1, 0.9, 'day 2'],
+ [2, 0.3, 'day 1'], [2, 0.6, 'day 1'], [2, 0.6, 'day 1'], [2, 0.9, 'day 1']
+ ]);
+ assertRanges([['day 1', 'day 2'], [0.27, 1.02]]);
+ },
+ null, BOXEVENTS, 'violin select'
+ );
+ })
+ .catch(fail)
+ .then(done);
+ });
});
// to make sure none of the above tests fail with extraneous invisible traces,
diff --git a/test/jasmine/tests/violin_test.js b/test/jasmine/tests/violin_test.js
new file mode 100644
index 00000000000..755d2b20e19
--- /dev/null
+++ b/test/jasmine/tests/violin_test.js
@@ -0,0 +1,476 @@
+var Plotly = require('@lib');
+var Lib = require('@src/lib');
+var Plots = require('@src/plots/plots');
+
+var Violin = require('@src/traces/violin');
+
+var d3 = require('d3');
+var createGraphDiv = require('../assets/create_graph_div');
+var destroyGraphDiv = require('../assets/destroy_graph_div');
+var fail = require('../assets/fail_test');
+var mouseEvent = require('../assets/mouse_event');
+
+var customAssertions = require('../assets/custom_assertions');
+var assertHoverLabelContent = customAssertions.assertHoverLabelContent;
+
+describe('Test violin defaults', function() {
+ var traceOut;
+
+ function _supply(traceIn, layout) {
+ traceOut = {};
+ Violin.supplyDefaults(traceIn, traceOut, '#444', layout || {});
+ }
+
+ it('should set visible to false when x and y are empty', function() {
+ _supply({
+ x: [],
+ y: []
+ });
+
+ expect(traceOut.visible).toBe(false);
+ });
+
+ it('should inherit layout.calendar', function() {
+ _supply({y: [1, 2, 3]}, {calendar: 'islamic'});
+
+ // we always fill calendar attributes, because it's hard to tell if
+ // we're on a date axis at this point.
+ expect(traceOut.xcalendar).toBe('islamic');
+ expect(traceOut.ycalendar).toBe('islamic');
+ });
+
+ it('should take its own calendars', function() {
+ _supply({
+ y: [1, 2, 3],
+ xcalendar: 'coptic',
+ ycalendar: 'ethiopian'
+ }, {
+ calendar: 'islamic'
+ });
+
+ // we always fill calendar attributes, because it's hard to tell if
+ // we're on a date axis at this point.
+ expect(traceOut.xcalendar).toBe('coptic');
+ expect(traceOut.ycalendar).toBe('ethiopian');
+ });
+
+ it('should not coerce point attributes when *points* is false', function() {
+ _supply({
+ y: [1, 1, 2],
+ points: false
+ });
+
+ expect(traceOut.points).toBe(false);
+ expect(traceOut.jitter).toBeUndefined();
+ expect(traceOut.pointpos).toBeUndefined();
+ expect(traceOut.marker).toBeUndefined();
+ expect(traceOut.text).toBeUndefined();
+ });
+
+ it('should default *points* to suspectedoutliers when marker.outliercolor is set & valid', function() {
+ _supply({
+ y: [1, 1, 2],
+ marker: { outliercolor: 'blue' }
+ });
+
+ expect(traceOut.points).toBe('suspectedoutliers');
+ });
+
+ it('should default *points* to suspectedoutliers when marker.line.outliercolor is set & valid', function() {
+ _supply({
+ y: [1, 1, 2],
+ marker: { line: {outliercolor: 'blue'} }
+ });
+
+ expect(traceOut.points).toBe('suspectedoutliers');
+ expect(traceOut.marker).toBeDefined();
+ expect(traceOut.text).toBeDefined();
+ });
+
+ it('should default *spanmode* to manual when *span* is set to an array', function() {
+ _supply({
+ y: [1, 1, 2],
+ span: [0, 1]
+ });
+ expect(traceOut.span).toEqual([0, 1]);
+ expect(traceOut.spanmode).toBe('manual');
+
+ _supply({
+ x: [1, 1, 2],
+ span: 'not-gonna-work'
+ });
+ expect(traceOut.span).toBeUndefined();
+ expect(traceOut.spanmode).toBe('soft');
+ });
+
+ it('should default *.visible attributes when one of their corresponding style attributes is set & valid', function() {
+ _supply({
+ y: [1, 2, 1],
+ box: { width: 0.1 },
+ meanline: { color: 'red' }
+ });
+ expect(traceOut.box.visible).toBe(true);
+ expect(traceOut.meanline.visible).toBe(true);
+
+ _supply({
+ y: [1, 2, 1],
+ box: {
+ visible: false,
+ width: 0.1
+ },
+ meanline: {
+ visible: false,
+ color: 'red'
+ }
+ });
+ expect(traceOut.box).toBeUndefined();
+ expect(traceOut.meanline).toBeUndefined();
+ });
+
+ it('should use violin style settings to default inner style attribute', function() {
+ _supply({
+ y: [1, 2, 1],
+ fillcolor: 'red',
+ line: {color: 'blue', width: 10},
+ box: {visible: true},
+ meanline: {visible: true}
+ });
+ expect(traceOut.box.fillcolor).toBe('red');
+ expect(traceOut.box.line.color).toBe('blue');
+ expect(traceOut.box.line.width).toBe(10);
+ expect(traceOut.meanline.color).toBe('blue');
+ expect(traceOut.meanline.width).toBe(10);
+ });
+});
+
+describe('Test violin calc:', function() {
+ var cd, fullLayout;
+
+ function _calc(attrs, layout) {
+ var gd = {
+ data: [Lib.extendFlat({type: 'violin'}, attrs)],
+ layout: layout || {},
+ calcdata: []
+ };
+ Plots.supplyDefaults(gd);
+ Plots.doCalcdata(gd);
+ cd = gd.calcdata[0];
+ fullLayout = gd._fullLayout;
+ }
+
+ it('should compute bandwidth and span based on the sample and *spanmode*', function() {
+ var y = [1, 1, 2, 2, 3];
+
+ _calc({y: y});
+ expect(cd[0].bandwidth).toBeCloseTo(0.64);
+ expect(cd[0].span).toBeCloseToArray([-0.28, 4.28]);
+
+ _calc({
+ y: y,
+ spanmode: 'hard'
+ });
+ expect(cd[0].span).toBeCloseToArray([1, 3]);
+
+ _calc({
+ y: y,
+ span: [0, 0]
+ });
+ expect(cd[0].span).toBeCloseToArray([-1, 1], 'cleans up invalid range');
+
+ _calc({
+ y: y,
+ span: [null, 5]
+ });
+ expect(cd[0].span).toBeCloseToArray([-0.28, 5], 'defaults to soft bound');
+
+ _calc({
+ y: y,
+ span: [0, null]
+ });
+ expect(cd[0].span).toBeCloseToArray([0, 4.28], 'defaults to soft bound');
+ });
+
+ it('should honor set bandwidth in span calculations', function() {
+ var y = [1, 1, 2, 2, 3];
+ var bw = 0.1;
+
+ _calc({
+ y: y,
+ bandwidth: bw
+ });
+ expect(cd[0].bandwidth).toBeCloseTo(0.1);
+ expect(cd[0].span).toBeCloseToArray([0.8, 3.2]);
+
+ _calc({
+ y: y,
+ bandwidth: bw,
+ spanmode: 'hard'
+ });
+ expect(cd[0].span).toBeCloseToArray([1, 3]);
+
+ _calc({
+ y: y,
+ bandwidth: bw,
+ span: [0, 0]
+ });
+ expect(cd[0].span).toBeCloseToArray([-1, 1], 'cleans up invalid range');
+
+ _calc({
+ y: y,
+ bandwidth: bw,
+ span: [null, 5]
+ });
+ expect(cd[0].span).toBeCloseToArray([0.8, 5], 'defaults to soft bound');
+
+ _calc({
+ y: y,
+ bandwidth: bw,
+ span: [0, null]
+ });
+ expect(cd[0].span).toBeCloseToArray([0, 3.2], 'defaults to soft bound');
+ });
+
+ it('should fill in scale-group stats', function() {
+ _calc({
+ name: 'one',
+ y: [0, 0, 0, 0, 10, 10, 10, 10]
+ });
+ expect(fullLayout._violinScaleGroupStats.one.maxWidth).toBeCloseTo(0.055);
+ expect(fullLayout._violinScaleGroupStats.one.maxCount).toBe(8);
+ });
+});
+
+describe('Test violin hover:', function() {
+ var gd;
+
+ afterEach(destroyGraphDiv);
+
+ function run(specs) {
+ gd = createGraphDiv();
+
+ var fig = Lib.extendDeep(
+ {width: 700, height: 500},
+ specs.mock || require('@mocks/violin_grouped.json')
+ );
+
+ if(specs.patch) {
+ fig = specs.patch(fig);
+ }
+
+ var pos = specs.pos || [200, 200];
+
+ return Plotly.plot(gd, fig).then(function() {
+ mouseEvent('mousemove', pos[0], pos[1]);
+ assertHoverLabelContent(specs);
+ });
+ }
+
+ [{
+ desc: 'base',
+ nums: [
+ 'median: 0.55', 'min: 0', 'q1: 0.3', 'q3: 0.6', 'max: 0.7',
+ 'y: 0.9266848, kde: 0.182'
+ ],
+ name: ['radishes', '', '', '', '', ''],
+ axis: 'day 1'
+ }, {
+ desc: 'with mean',
+ patch: function(fig) {
+ fig.data.forEach(function(trace) {
+ trace.meanline = {visible: true};
+ });
+ return fig;
+ },
+ nums: [
+ 'median: 0.55', 'min: 0', 'q1: 0.3', 'q3: 0.6', 'max: 0.7', 'mean: 0.45',
+ 'y: 0.9266848, kde: 0.182'
+ ],
+ name: ['radishes', '', '', '', '', '', ''],
+ axis: 'day 1'
+ }, {
+ desc: 'with overlaid violins',
+ patch: function(fig) {
+ fig.layout.violinmode = 'overlay';
+ return fig;
+ },
+ nums: [
+ 'q3: 0.6', 'median: 0.45', 'q3: 0.6', 'max: 1', 'y: 0.9266848, kde: 0.383',
+ 'median: 0.55', 'max: 0.7', 'y: 0.9266848, kde: 0.182',
+ 'median: 0.45', 'q3: 0.6', 'max: 0.9', 'y: 0.9266848, kde: 0.435',
+ 'q3: 0.6', 'max: 0.9'
+ ],
+ name: [
+ '', 'kale', '', '', '', 'radishes', '',
+ '', 'carrots', '', '', ''
+ ],
+ axis: 'day 1'
+ }, {
+ desc: 'hoveron points | hovermode closest',
+ patch: function(fig) {
+ fig.data.forEach(function(trace) {
+ trace.points = 'all';
+ trace.hoveron = 'points';
+ });
+ fig.layout.hovermode = 'closest';
+ return fig;
+ },
+ pos: [220, 200],
+ nums: '(day 1, 0.9)',
+ name: 'carrots'
+ }, {
+ desc: 'hoveron points | hovermode x',
+ patch: function(fig) {
+ fig.data.forEach(function(trace) {
+ trace.points = 'all';
+ trace.hoveron = 'points';
+ });
+ fig.layout.hovermode = 'x';
+ return fig;
+ },
+ pos: [220, 200],
+ nums: '0.9',
+ name: 'carrots',
+ axis: 'day 1'
+ }, {
+ desc: 'hoveron violins+points | hovermode x (hover on violin only - same result as base)',
+ patch: function(fig) {
+ fig.data.forEach(function(trace) {
+ trace.points = 'all';
+ trace.hoveron = 'points+violins';
+ });
+ fig.layout.hovermode = 'x';
+ return fig;
+ },
+ nums: ['median: 0.55', 'min: 0', 'q1: 0.3', 'q3: 0.6', 'max: 0.7'],
+ name: ['radishes', '', '', '', ''],
+ axis: 'day 1'
+ }, {
+ desc: 'hoveron violins+points | hovermode x (violin AND closest point)',
+ patch: function(fig) {
+ fig.data.forEach(function(trace) {
+ trace.points = 'all';
+ trace.hoveron = 'points+violins';
+ trace.pointpos = 0;
+ });
+ fig.layout.hovermode = 'x';
+ return fig;
+ },
+ pos: [207, 240],
+ nums: ['0.7', 'median: 0.55', 'min: 0', 'q1: 0.3', 'q3: 0.6', 'max: 0.7'],
+ name: ['radishes', 'radishes', '', '', '', ''],
+ axis: 'day 1'
+ }, {
+ desc: 'text items on hover',
+ patch: function(fig) {
+ fig.data.forEach(function(trace) {
+ trace.points = 'all';
+ trace.hoveron = 'points';
+ trace.text = trace.y.map(function(v) { return 'look:' + v; });
+ });
+ fig.layout.hovermode = 'closest';
+ return fig;
+ },
+ pos: [180, 240],
+ nums: '(day 1, 0.7)\nlook:0.7',
+ name: 'radishes'
+ }, {
+ desc: 'only text items on hover',
+ patch: function(fig) {
+ fig.data.forEach(function(trace) {
+ trace.points = 'all';
+ trace.hoveron = 'points';
+ trace.text = trace.y.map(function(v) { return 'look:' + v; });
+ trace.hoverinfo = 'text';
+ });
+ fig.layout.hovermode = 'closest';
+ return fig;
+ },
+ pos: [180, 240],
+ nums: 'look:0.7',
+ name: ''
+ }, {
+ desc: 'one-sided violin under hovermode closest',
+ // hoveron: 'kde+points'
+ // hovermode: 'closest'
+ // width: 400
+ // height: 700
+ mock: require('@mocks/violin_side-by-side.json'),
+ pos: [250, 300],
+ nums: '(x: 42.43046, kde: 0.083, Saturday)',
+ name: ''
+ }, {
+ desc: 'one-sided violin under hovermode y',
+ // hoveron: 'kde+points'
+ // width: 400
+ // height: 700
+ mock: require('@mocks/violin_side-by-side.json'),
+ patch: function(fig) {
+ fig.layout.hovermode = 'y';
+ return fig;
+ },
+ pos: [250, 300],
+ nums: 'x: 42.43046, kde: 0.083',
+ name: '',
+ axis: 'Saturday'
+ }]
+ .forEach(function(specs) {
+ it('should generate correct hover labels ' + specs.desc, function(done) {
+ run(specs).catch(fail).then(done);
+ });
+ });
+
+ describe('KDE lines inside violin under *kde* hoveron flag', function() {
+ var fig;
+
+ beforeEach(function() {
+ gd = createGraphDiv();
+
+ fig = Lib.extendDeep({}, require('@mocks/violin_old-faithful.json'), {
+ layout: {width: 500, height: 500}
+ });
+ fig.data[0].points = false;
+ });
+
+ function assertViolinHoverLine(pos) {
+ var line = d3.select('.hoverlayer').selectAll('line');
+
+ expect(line.size()).toBe(1, 'only one violin line at a time');
+ expect(line.attr('class').indexOf('violinline')).toBe(0, 'correct class name');
+ expect([
+ line.attr('x1'), line.attr('y1'),
+ line.attr('x2'), line.attr('y2')
+ ]).toBeCloseToArray(pos, 'line position');
+ }
+
+ it('should show in two-sided base case', function(done) {
+ Plotly.plot(gd, fig).then(function() {
+ mouseEvent('mousemove', 250, 250);
+ assertViolinHoverLine([299.35, 250, 200.65, 250]);
+ })
+ .catch(fail)
+ .then(done);
+ });
+
+ it('should show in one-sided positive case', function(done) {
+ fig.data[0].side = 'positive';
+
+ Plotly.plot(gd, fig).then(function() {
+ mouseEvent('mousemove', 300, 250);
+ assertViolinHoverLine([299.35, 250, 250, 250]);
+ })
+ .catch(fail)
+ .then(done);
+ });
+
+ it('should show in one-sided negative case', function(done) {
+ fig.data[0].side = 'negative';
+
+ Plotly.plot(gd, fig).then(function() {
+ mouseEvent('mousemove', 200, 250);
+ assertViolinHoverLine([200.65, 250, 250, 250]);
+ })
+ .catch(fail)
+ .then(done);
+ });
+ });
+});