diff --git a/src/traces/box/attributes.js b/src/traces/box/attributes.js
index a70e1c257d3..7bfd9bce3ce 100644
--- a/src/traces/box/attributes.js
+++ b/src/traces/box/attributes.js
@@ -171,6 +171,20 @@ module.exports = {
             'the vertical (horizontal).'
         ].join(' ')
     },
+
+    width: {
+        valType: 'number',
+        min: 0,
+        role: 'info',
+        dflt: 0,
+        editType: 'calc',
+        description: [
+            'Sets the width of the box in data coordinate',
+            'If *0* (default value) the width is automatically selected based on the positions',
+            'of other box traces in the same subplot.'
+        ].join(' ')
+    },
+
     marker: {
         outliercolor: {
             valType: 'color',
@@ -244,7 +258,6 @@ module.exports = {
         marker: scatterAttrs.unselected.marker,
         editType: 'style'
     },
-
     hoveron: {
         valType: 'flaglist',
         flags: ['boxes', 'points'],
diff --git a/src/traces/box/calc.js b/src/traces/box/calc.js
index 517c2b3d089..02fa7a15d0e 100644
--- a/src/traces/box/calc.js
+++ b/src/traces/box/calc.js
@@ -64,6 +64,11 @@ module.exports = function calc(gd, trace) {
         }
     }
 
+    var cdi;
+    var ptFilterFn = (trace.boxpoints || trace.points) === 'all' ?
+        Lib.identity :
+        function(pt) { return (pt.v < cdi.lf || pt.v > cdi.uf); };
+
     // build calcdata trace items, one item per distinct position
     for(i = 0; i < pLen; i++) {
         if(ptsPerBin[i].length > 0) {
@@ -71,10 +76,9 @@ module.exports = function calc(gd, trace) {
             var boxVals = pts.map(extractVal);
             var bvLen = boxVals.length;
 
-            var cdi = {
-                pos: posDistinct[i],
-                pts: pts
-            };
+            cdi = {};
+            cdi.pos = posDistinct[i];
+            cdi.pts = pts;
 
             cdi.min = boxVals[0];
             cdi.max = boxVals[bvLen - 1];
@@ -110,13 +114,14 @@ module.exports = function calc(gd, trace) {
             cdi.lo = 4 * cdi.q1 - 3 * cdi.q3;
             cdi.uo = 4 * cdi.q3 - 3 * cdi.q1;
 
-
             // lower and upper notches ~95% Confidence Intervals for median
             var iqr = cdi.q3 - cdi.q1;
             var mci = 1.57 * iqr / Math.sqrt(bvLen);
             cdi.ln = cdi.med - mci;
             cdi.un = cdi.med + mci;
 
+            cdi.pts2 = pts.filter(ptFilterFn);
+
             cd.push(cdi);
         }
     }
diff --git a/src/traces/box/cross_trace_calc.js b/src/traces/box/cross_trace_calc.js
index f7ad3de3561..2d42b0b0c74 100644
--- a/src/traces/box/cross_trace_calc.js
+++ b/src/traces/box/cross_trace_calc.js
@@ -22,8 +22,6 @@ function crossTraceCalc(gd, plotinfo) {
         var orientation = orientations[i];
         var posAxis = orientation === 'h' ? ya : xa;
         var boxList = [];
-        var minPad = 0;
-        var maxPad = 0;
 
         // make list of boxes / candlesticks
         // For backward compatibility, candlesticks are treated as if they *are* box traces here
@@ -40,72 +38,173 @@ function crossTraceCalc(gd, plotinfo) {
                     trace.yaxis === ya._id
               ) {
                 boxList.push(j);
-
-                if(trace.boxpoints) {
-                    minPad = Math.max(minPad, trace.jitter - trace.pointpos - 1);
-                    maxPad = Math.max(maxPad, trace.jitter + trace.pointpos - 1);
-                }
             }
         }
 
-        setPositionOffset('box', gd, boxList, posAxis, [minPad, maxPad]);
+        setPositionOffset('box', gd, boxList, posAxis);
     }
 }
 
-function setPositionOffset(traceType, gd, boxList, posAxis, pad) {
+function setPositionOffset(traceType, gd, boxList, posAxis) {
     var calcdata = gd.calcdata;
     var fullLayout = gd._fullLayout;
-    var pointList = [];
+    var axId = posAxis._id;
+    var axLetter = axId.charAt(0);
 
     // N.B. reused in violin
     var numKey = traceType === 'violin' ? '_numViolins' : '_numBoxes';
 
     var i, j, calcTrace;
+    var pointList = [];
+    var shownPts = 0;
 
     // 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);
+            shownPts += (calcTrace[j].pts2 || []).length;
         }
     }
 
     if(!pointList.length) return;
 
     // 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;
-    }
+    var dPos0 = boxdv.minDiff / 2;
 
     // check for forced minimum dtick
     Axes.minDtick(posAxis, boxdv.minDiff, boxdv.vals[0], true);
 
-    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
-    var extremes = Axes.findExtremes(posAxis, boxdv.vals, {
-        vpadminus: dPos + pad[0] * padfactor,
-        vpadplus: dPos + pad[1] * padfactor
-    });
+    var num = fullLayout[numKey];
+    var group = (fullLayout[traceType + 'mode'] === 'group' && num > 1);
+    var groupFraction = 1 - fullLayout[traceType + 'gap'];
+    var groupGapFraction = 1 - fullLayout[traceType + 'groupgap'];
 
     for(i = 0; i < boxList.length; i++) {
         calcTrace = calcdata[boxList[i]];
-        // set the width of all boxes
-        calcTrace[0].t.dPos = dPos;
-        // link extremes to all boxes
-        calcTrace[0].trace._extremes[posAxis._id] = extremes;
-    }
 
+        var trace = calcTrace[0].trace;
+        var t = calcTrace[0].t;
+        var width = trace.width;
+        var side = trace.side;
+
+        // position coordinate delta
+        var dPos;
+        // box half width;
+        var bdPos;
+        // box center offset
+        var bPos;
+        // half-width within which to accept hover for this box/violin
+        // always split the distance to the closest box/violin
+        var wHover;
+
+        if(width) {
+            dPos = bdPos = wHover = width / 2;
+            bPos = 0;
+        } else {
+            dPos = dPos0;
+            bdPos = dPos * groupFraction * groupGapFraction / (group ? num : 1);
+            bPos = group ? 2 * dPos * (-0.5 + (t.num + 0.5) / num) * groupFraction : 0;
+            wHover = dPos * (group ? groupFraction / num : 1);
+        }
+        t.dPos = dPos;
+        t.bPos = bPos;
+        t.bdPos = bdPos;
+        t.wHover = wHover;
+
+        // box/violin-only value-space push value
+        var pushplus;
+        var pushminus;
+        // edge of box/violin
+        var edge = bPos + bdPos;
+        var edgeplus;
+        var edgeminus;
+
+        if(side === 'positive') {
+            pushplus = dPos * (width ? 1 : 0.5);
+            edgeplus = edge;
+            pushminus = edgeplus = bPos;
+        } else if(side === 'negative') {
+            pushplus = edgeplus = bPos;
+            pushminus = dPos * (width ? 1 : 0.5);
+            edgeminus = edge;
+        } else {
+            pushplus = pushminus = dPos;
+            edgeplus = edgeminus = edge;
+        }
+
+        // value-space padding
+        var vpadplus;
+        var vpadminus;
+        // pixel-space padding
+        var ppadplus;
+        var ppadminus;
+        // do we add 5% of both sides (for points beyond box/violin)
+        var padded = false;
+        // does this trace show points?
+        var hasPts = (trace.boxpoints || trace.points) && (shownPts > 0);
+
+        if(hasPts) {
+            var pointpos = trace.pointpos;
+            var jitter = trace.jitter;
+            var ms = trace.marker.size / 2;
+
+            var pp = 0;
+            if((pointpos + jitter) >= 0) {
+                pp = edge * (pointpos + jitter);
+                if(pp > pushplus) {
+                    // (++) beyond plus-value, use pp
+                    padded = true;
+                    ppadplus = ms;
+                    vpadplus = pp;
+                } else if(pp > edgeplus) {
+                    // (+), use push-value (it's bigger), but add px-pad
+                    ppadplus = ms;
+                    vpadplus = pushplus;
+                }
+            }
+            if(pp <= pushplus) {
+                // (->) fallback to push value
+                vpadplus = pushplus;
+            }
+
+            var pm = 0;
+            if((pointpos - jitter) <= 0) {
+                pm = -edge * (pointpos - jitter);
+                if(pm > pushminus) {
+                    // (--) beyond plus-value, use pp
+                    padded = true;
+                    ppadminus = ms;
+                    vpadminus = pm;
+                } else if(pm > edgeminus) {
+                    // (-), use push-value (it's bigger), but add px-pad
+                    ppadminus = ms;
+                    vpadminus = pushminus;
+                }
+            }
+            if(pm <= pushminus) {
+                // (<-) fallback to push value
+                vpadminus = pushminus;
+            }
+        } else {
+            vpadplus = pushplus;
+            vpadminus = pushminus;
+        }
+
+        // calcdata[i][j] are in ascending order
+        var firstPos = calcTrace[0].pos;
+        var lastPos = calcTrace[calcTrace.length - 1].pos;
+
+        trace._extremes[axId] = Axes.findExtremes(posAxis, [firstPos, lastPos], {
+            padded: padded,
+            vpadminus: vpadminus,
+            vpadplus: vpadplus,
+            // N.B. SVG px-space positive/negative
+            ppadminus: {x: ppadminus, y: ppadplus}[axLetter],
+            ppadplus: {x: ppadplus, y: ppadminus}[axLetter],
+        });
+    }
 }
 
 module.exports = {
diff --git a/src/traces/box/defaults.js b/src/traces/box/defaults.js
index 7e7ec2d642e..b3d6500e5d4 100644
--- a/src/traces/box/defaults.js
+++ b/src/traces/box/defaults.js
@@ -28,6 +28,7 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) {
 
     coerce('whiskerwidth');
     coerce('boxmean');
+    coerce('width');
 
     var notched = coerce('notched', traceIn.notchwidth !== undefined);
     if(notched) coerce('notchwidth');
diff --git a/src/traces/box/layout_attributes.js b/src/traces/box/layout_attributes.js
index 2e1ec93dedf..d09052b11be 100644
--- a/src/traces/box/layout_attributes.js
+++ b/src/traces/box/layout_attributes.js
@@ -22,7 +22,8 @@ module.exports = {
             'If *group*, the boxes are plotted next to one another',
             'centered around the shared location.',
             'If *overlay*, the boxes are plotted over one another,',
-            'you might need to set *opacity* to see them multiple boxes.'
+            'you might need to set *opacity* to see them multiple boxes.',
+            'Has no effect on traces that have *width* set.'
         ].join(' ')
     },
     boxgap: {
@@ -34,7 +35,8 @@ module.exports = {
         editType: 'calc',
         description: [
             'Sets the gap (in plot fraction) between boxes of',
-            'adjacent location coordinates.'
+            'adjacent location coordinates.',
+            'Has no effect on traces that have *width* set.'
         ].join(' ')
     },
     boxgroupgap: {
@@ -46,7 +48,8 @@ module.exports = {
         editType: 'calc',
         description: [
             'Sets the gap (in plot fraction) between boxes of',
-            'the same location coordinate.'
+            'the same location coordinate.',
+            'Has no effect on traces that have *width* set.'
         ].join(' ')
     }
 };
diff --git a/src/traces/box/plot.js b/src/traces/box/plot.js
index c04d1d8266b..6d7f03715aa 100644
--- a/src/traces/box/plot.js
+++ b/src/traces/box/plot.js
@@ -18,12 +18,8 @@ var JITTERCOUNT = 5; // points either side of this to include
 var JITTERSPREAD = 0.01; // fraction of IQR to count as "dense"
 
 function plot(gd, plotinfo, cdbox, boxLayer) {
-    var fullLayout = gd._fullLayout;
     var xa = plotinfo.xaxis;
     var ya = plotinfo.yaxis;
-    var numBoxes = fullLayout._numBoxes;
-    var groupFraction = (1 - fullLayout.boxgap);
-    var group = (fullLayout.boxmode === 'group' && numBoxes > 1);
 
     Lib.makeTraceGroups(boxLayer, cdbox, 'trace boxes').each(function(cd) {
         var plotGroup = d3.select(this);
@@ -31,12 +27,9 @@ function plot(gd, plotinfo, cdbox, boxLayer) {
         var t = cd0.t;
         var trace = cd0.trace;
         if(!plotinfo.isRangePlot) cd0.node3 = plotGroup;
-        // box half width
-        var bdPos = t.dPos * groupFraction * (1 - fullLayout.boxgroupgap) / (group ? numBoxes : 1);
-        // box center offset
-        var bPos = group ? 2 * t.dPos * (-0.5 + (t.num + 0.5) / numBoxes) * groupFraction : 0;
+
         // whisker width
-        var wdPos = bdPos * trace.whiskerwidth;
+        t.wdPos = t.bdPos * trace.whiskerwidth;
 
         if(trace.visible !== true || t.empty) {
             plotGroup.remove();
@@ -53,14 +46,6 @@ function plot(gd, plotinfo, cdbox, boxLayer) {
             valAxis = ya;
         }
 
-        // save the box size and box position for use by hover
-        t.bPos = bPos;
-        t.bdPos = bdPos;
-        t.wdPos = wdPos;
-        // half-width within which to accept hover for this box
-        // always split the distance to the closest box
-        t.wHover = t.dPos * (group ? groupFraction / numBoxes : 1);
-
         plotBoxAndWhiskers(plotGroup, {pos: posAxis, val: valAxis}, trace, t);
         plotPoints(plotGroup, {x: xa, y: ya}, trace, t);
         plotBoxMean(plotGroup, {pos: posAxis, val: valAxis}, trace, t);
@@ -192,10 +177,7 @@ function plotPoints(sel, axes, trace, t) {
     var paths = gPoints.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); });
+            var pts = d.pts2;
 
             // 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);
diff --git a/src/traces/violin/attributes.js b/src/traces/violin/attributes.js
index 1898cb11559..ba71026aadf 100644
--- a/src/traces/violin/attributes.js
+++ b/src/traces/violin/attributes.js
@@ -135,6 +135,15 @@ module.exports = {
             'right (left) for vertical violins and above (below) for horizontal violins.'
         ].join(' ')
     }),
+
+    width: extendFlat({}, boxAttrs.width, {
+        description: [
+            'Sets the width of the violin in data coordinates.',
+            'If *0* (default value) the width is automatically selected based on the positions',
+            'of other violin traces in the same subplot.',
+        ].join(' ')
+    }),
+
     marker: boxAttrs.marker,
     text: boxAttrs.text,
 
@@ -220,7 +229,7 @@ module.exports = {
         values: ['both', 'positive', 'negative'],
         dflt: 'both',
         role: 'info',
-        editType: 'plot',
+        editType: 'calc',
         description: [
             'Determines on which side of the position value the density function making up',
             'one half of a violin is plotted.',
diff --git a/src/traces/violin/calc.js b/src/traces/violin/calc.js
index bdb2193f0d9..2926a97a5f0 100644
--- a/src/traces/violin/calc.js
+++ b/src/traces/violin/calc.js
@@ -25,18 +25,10 @@ module.exports = function calc(gd, trace) {
         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
-        };
-    }
-
     var spanMin = Infinity;
     var spanMax = -Infinity;
+    var maxKDE = 0;
+    var maxCount = 0;
 
     for(var i = 0; i < cd.length; i++) {
         var cdi = cd[i];
@@ -61,12 +53,11 @@ module.exports = function calc(gd, trace) {
 
         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};
+            maxKDE = Math.max(maxKDE, v);
         }
 
-        groupStats.maxCount = Math.max(groupStats.maxCount, vals.length);
-
+        maxCount = Math.max(maxCount, vals.length);
         spanMin = Math.min(spanMin, span[0]);
         spanMax = Math.max(spanMax, span[1]);
     }
@@ -74,6 +65,24 @@ module.exports = function calc(gd, trace) {
     var extremes = Axes.findExtremes(valAxis, [spanMin, spanMax], {padded: true});
     trace._extremes[valAxis._id] = extremes;
 
+    if(trace.width) {
+        cd[0].t.maxKDE = maxKDE;
+    } else {
+        var violinScaleGroupStats = fullLayout._violinScaleGroupStats;
+        var scaleGroup = trace.scalegroup;
+        var groupStats = violinScaleGroupStats[scaleGroup];
+
+        if(groupStats) {
+            groupStats.maxKDE = Math.max(groupStats.maxKDE, maxKDE);
+            groupStats.maxCount = Math.max(groupStats.maxCount, maxCount);
+        } else {
+            violinScaleGroupStats[scaleGroup] = {
+                maxKDE: maxKDE,
+                maxCount: maxCount
+            };
+        }
+    }
+
     cd[0].t.labels.kde = Lib._(gd, 'kde:');
 
     return cd;
diff --git a/src/traces/violin/cross_trace_calc.js b/src/traces/violin/cross_trace_calc.js
index df04239e697..2463233faf2 100644
--- a/src/traces/violin/cross_trace_calc.js
+++ b/src/traces/violin/cross_trace_calc.js
@@ -20,8 +20,6 @@ module.exports = function crossTraceCalc(gd, plotinfo) {
         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];
@@ -35,14 +33,9 @@ module.exports = function crossTraceCalc(gd, plotinfo) {
                     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]);
+        setPositionOffset('violin', gd, violinList, posAxis);
     }
 };
diff --git a/src/traces/violin/defaults.js b/src/traces/violin/defaults.js
index d79d42bac70..0a0fa8d1a73 100644
--- a/src/traces/violin/defaults.js
+++ b/src/traces/violin/defaults.js
@@ -26,10 +26,14 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
     if(traceOut.visible === false) return;
 
     coerce('bandwidth');
-    coerce('scalegroup', traceOut.name);
-    coerce('scalemode');
     coerce('side');
 
+    var width = coerce('width');
+    if(!width) {
+        coerce('scalegroup', traceOut.name);
+        coerce('scalemode');
+    }
+
     var span = coerce('span');
     var spanmodeDflt;
     if(Array.isArray(span)) spanmodeDflt = 'manual';
diff --git a/src/traces/violin/layout_attributes.js b/src/traces/violin/layout_attributes.js
index b2800f346e7..9bb04cb19c0 100644
--- a/src/traces/violin/layout_attributes.js
+++ b/src/traces/violin/layout_attributes.js
@@ -19,19 +19,22 @@ module.exports = {
             '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.'
+            'you might need to set *opacity* to see them multiple violins.',
+            'Has no effect on traces that have *width* set.'
         ].join(' ')
     }),
     violingap: extendFlat({}, boxLayoutAttrs.boxgap, {
         description: [
             'Sets the gap (in plot fraction) between violins of',
-            'adjacent location coordinates.'
+            'adjacent location coordinates.',
+            'Has no effect on traces that have *width* set.'
         ].join(' ')
     }),
     violingroupgap: extendFlat({}, boxLayoutAttrs.boxgroupgap, {
         description: [
             'Sets the gap (in plot fraction) between violins of',
-            'the same location coordinate.'
+            'the same location coordinate.',
+            'Has no effect on traces that have *width* set.'
         ].join(' ')
     })
 };
diff --git a/src/traces/violin/plot.js b/src/traces/violin/plot.js
index 4848305dd90..10762814a92 100644
--- a/src/traces/violin/plot.js
+++ b/src/traces/violin/plot.js
@@ -39,28 +39,19 @@ module.exports = function plot(gd, plotinfo, cdViolins, violinLayer) {
         var t = cd0.t;
         var trace = cd0.trace;
         if(!plotinfo.isRangePlot) cd0.node3 = plotGroup;
-        var numViolins = fullLayout._numViolins;
-        var group = (fullLayout.violinmode === 'group' && numViolins > 1);
-        var groupFraction = 1 - fullLayout.violingap;
-        // violin max half width
-        var bdPos = t.bdPos = t.dPos * groupFraction * (1 - fullLayout.violingroupgap) / (group ? numViolins : 1);
-        // violin center offset
-        var bPos = t.bPos = group ? 2 * t.dPos * (-0.5 + (t.num + 0.5) / numViolins) * groupFraction : 0;
-        // half-width within which to accept hover for this violin
-        // always split the distance to the closest violin
-        t.wHover = t.dPos * (group ? groupFraction / numViolins : 1);
 
         if(trace.visible !== true || t.empty) {
             plotGroup.remove();
             return;
         }
 
+        var bPos = t.bPos;
+        var bdPos = t.bdPos;
         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 groupStats = fullLayout._violinScaleGroupStats[trace.scalegroup];
 
         var violins = plotGroup.selectAll('path.violin').data(Lib.identity);
 
@@ -76,15 +67,15 @@ module.exports = function plot(gd, plotinfo, cdViolins, violinLayer) {
             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 scale;
+            if(trace.width) {
+                scale = t.maxKDE / bdPos;
+            } else {
+                var groupStats = fullLayout._violinScaleGroupStats[trace.scalegroup];
+                scale = trace.scalemode === 'count' ?
+                    (groupStats.maxKDE / bdPos) * (groupStats.maxCount / d.pts.length) :
+                    groupStats.maxKDE / bdPos;
             }
 
             var pathPos, pathNeg, path;
diff --git a/test/image/baselines/10.png b/test/image/baselines/10.png
index 475b193bcb5..aaf0100074e 100644
Binary files a/test/image/baselines/10.png and b/test/image/baselines/10.png differ
diff --git a/test/image/baselines/31.png b/test/image/baselines/31.png
index 1b4de09c01c..e47e81992ed 100644
Binary files a/test/image/baselines/31.png and b/test/image/baselines/31.png differ
diff --git a/test/image/baselines/box_grouped.png b/test/image/baselines/box_grouped.png
index fc7cf632651..f4b042cbca3 100644
Binary files a/test/image/baselines/box_grouped.png and b/test/image/baselines/box_grouped.png differ
diff --git a/test/image/baselines/box_grouped_horz.png b/test/image/baselines/box_grouped_horz.png
index 4c55500cd07..8dd083420e4 100644
Binary files a/test/image/baselines/box_grouped_horz.png and b/test/image/baselines/box_grouped_horz.png differ
diff --git a/test/image/baselines/box_plot_jitter_edge_cases.png b/test/image/baselines/box_plot_jitter_edge_cases.png
index b1fd5f70202..4467b3af805 100644
Binary files a/test/image/baselines/box_plot_jitter_edge_cases.png and b/test/image/baselines/box_plot_jitter_edge_cases.png differ
diff --git a/test/image/baselines/box_single-group.png b/test/image/baselines/box_single-group.png
new file mode 100644
index 00000000000..1c149acda8f
Binary files /dev/null and b/test/image/baselines/box_single-group.png differ
diff --git a/test/image/baselines/box_with-empty-1st-trace.png b/test/image/baselines/box_with-empty-1st-trace.png
index aaee8d92a53..caf7b7f1883 100644
Binary files a/test/image/baselines/box_with-empty-1st-trace.png and b/test/image/baselines/box_with-empty-1st-trace.png differ
diff --git a/test/image/baselines/hist_cum_stacked.png b/test/image/baselines/hist_cum_stacked.png
index 29702797e23..53297ad99e4 100644
Binary files a/test/image/baselines/hist_cum_stacked.png and b/test/image/baselines/hist_cum_stacked.png differ
diff --git a/test/image/baselines/point-selection2.png b/test/image/baselines/point-selection2.png
index 8a898869141..de39ff51820 100644
Binary files a/test/image/baselines/point-selection2.png and b/test/image/baselines/point-selection2.png differ
diff --git a/test/image/baselines/violin_box_multiple_widths.png b/test/image/baselines/violin_box_multiple_widths.png
new file mode 100644
index 00000000000..64a88204221
Binary files /dev/null and b/test/image/baselines/violin_box_multiple_widths.png differ
diff --git a/test/image/baselines/violin_box_overlay.png b/test/image/baselines/violin_box_overlay.png
index bf60201ef43..cae0accc9f9 100644
Binary files a/test/image/baselines/violin_box_overlay.png 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
index 30de14adb18..5a7937e41b4 100644
Binary files a/test/image/baselines/violin_grouped.png and b/test/image/baselines/violin_grouped.png differ
diff --git a/test/image/baselines/violin_negative_sides_w_points.png b/test/image/baselines/violin_negative_sides_w_points.png
new file mode 100644
index 00000000000..87f693f86e7
Binary files /dev/null and b/test/image/baselines/violin_negative_sides_w_points.png differ
diff --git a/test/image/baselines/violin_old-faithful.png b/test/image/baselines/violin_old-faithful.png
index 26d9b81943a..cc7b2e1040e 100644
Binary files a/test/image/baselines/violin_old-faithful.png and b/test/image/baselines/violin_old-faithful.png differ
diff --git a/test/image/baselines/violin_positive_and_negative.png b/test/image/baselines/violin_positive_and_negative.png
new file mode 100644
index 00000000000..aee3de868ee
Binary files /dev/null and b/test/image/baselines/violin_positive_and_negative.png differ
diff --git a/test/image/baselines/violin_positive_sides_w_points.png b/test/image/baselines/violin_positive_sides_w_points.png
new file mode 100644
index 00000000000..a5314c831c7
Binary files /dev/null and b/test/image/baselines/violin_positive_sides_w_points.png differ
diff --git a/test/image/baselines/violin_side-by-side.png b/test/image/baselines/violin_side-by-side.png
index 591ff342f6c..da309be00e9 100644
Binary files a/test/image/baselines/violin_side-by-side.png and b/test/image/baselines/violin_side-by-side.png differ
diff --git a/test/image/mocks/box_single-group.json b/test/image/mocks/box_single-group.json
new file mode 100644
index 00000000000..199044e9d7d
--- /dev/null
+++ b/test/image/mocks/box_single-group.json
@@ -0,0 +1,16 @@
+{
+  "data": [{
+    "type": "box",
+    "x0": 1,
+    "y": [1, 2, 1, 2, 1, 2, 3, 4, 4]
+  }, {
+    "type": "box",
+    "x0": 2,
+    "y": [2, 1, 2, 3, 3, 1, 0, 0, 1]
+  }],
+  "layout": {
+    "title": {"text": "single-box groups!"},
+    "boxmode": "group",
+    "showlegend": false
+  }
+}
diff --git a/test/image/mocks/violin_box_multiple_widths.json b/test/image/mocks/violin_box_multiple_widths.json
new file mode 100644
index 00000000000..60e0934ba2b
--- /dev/null
+++ b/test/image/mocks/violin_box_multiple_widths.json
@@ -0,0 +1,59 @@
+{
+    "data": [{
+            "type": "violin",
+            "width": 0.4,
+            "name": "width: 0.4",
+            "x": [0, 5, 7, 8],
+            "side": "positive",
+            "line": {
+                "color": "black"
+            },
+            "fillcolor": "#8dd3c7",
+            "opacity": 0.6,
+            "y0": 0.0
+        }, {
+            "type": "violin",
+            "name": "auto",
+            "x": [0, 5, 7, 8],
+            "side": "positive",
+            "line": {
+                "color": "black"
+            },
+            "fillcolor": "#d3c78d",
+            "opacity": 0.6,
+            "y0": 0.1
+        }, {
+            "type": "box",
+            "width": 0.6,
+            "name": "width: 0.6",
+            "x": [0, 5, 7, 8],
+            "side": "positive",
+            "line": {
+                "color": "black"
+            },
+            "fillcolor": "#c78dd3",
+            "opacity": 0.6,
+            "y0": 0.2
+        }, {
+            "type": "violin",
+            "width": 0.4,
+            "name": "width: 0.4 (solo)",
+            "x": [0, 5, 7, 8],
+            "side": "positive",
+            "line": {
+                "color": "black"
+            },
+            "fillcolor": "#8dd3c7",
+            "opacity": 0.6,
+            "y0": 0.0,
+            "xaxis": "x2",
+            "yaxis": "y2"
+    }],
+    "layout": {
+        "grid": {"rows": 1, "columns": 2, "pattern": "independent"},
+        "title": {"text" :"Violins/boxes - with multiple widths", "x": 0, "xref": "paper"},
+        "legend": {"x": 1, "y": 1, "xanchor": "right", "yanchor": "bottom"},
+        "xaxis": {"zeroline": false},
+        "yaxis": {"dtick": 0.1, "gridcolor": "black"}
+    }
+}
diff --git a/test/image/mocks/violin_negative_sides_w_points.json b/test/image/mocks/violin_negative_sides_w_points.json
new file mode 100644
index 00000000000..98b1723aa86
--- /dev/null
+++ b/test/image/mocks/violin_negative_sides_w_points.json
@@ -0,0 +1,23 @@
+{
+    "data": [{
+            "type": "violin",
+            "points": "all",
+            "pointpos": 1.5,
+            "marker": {"size": 12},
+            "jitter": 0,
+            "x": [0, 5, 7, 8],
+            "side": "negative",
+            "line": {
+                "color": "black"
+            },
+            "fillcolor": "#d3c78d",
+            "opacity": 0.6,
+            "y0": 0.0
+        }],
+    "layout": {
+        "title": "Violins - negative sided with positive points",
+        "legend": {"x": 0},
+        "xaxis": {"zeroline": false},
+        "yaxis": {"dtick": 0.1, "gridcolor": "black"}
+    }
+}
diff --git a/test/image/mocks/violin_positive_and_negative.json b/test/image/mocks/violin_positive_and_negative.json
new file mode 100644
index 00000000000..722e7303980
--- /dev/null
+++ b/test/image/mocks/violin_positive_and_negative.json
@@ -0,0 +1,35 @@
+{
+    "data": [{
+            "type": "violin",
+            "points": "all",
+            "pointpos": -0.3,
+            "jitter": 0,
+            "x": [0, 5, 7, 8],
+            "side": "positive",
+            "line": {
+                "color": "black"
+            },
+            "fillcolor": "#d3c78d",
+            "opacity": 0.6,
+            "y0": 0.0
+        },{
+            "type": "violin",
+            "points": "all",
+            "pointpos": 0,
+            "jitter": 0,
+            "x": [20, 25, 27, 28],
+            "side": "negative",
+            "line": {
+                "color": "black"
+            },
+            "fillcolor": "#d3c78d",
+            "opacity": 0.6,
+            "y0": 0.0
+        }],
+    "layout": {
+        "title": "Violins - positive and negative",
+        "showlegend": false,
+        "xaxis": {"zeroline": false},
+        "yaxis": {"dtick": 0.1, "gridcolor": "black"}
+    }
+}
diff --git a/test/image/mocks/violin_positive_sides_w_points.json b/test/image/mocks/violin_positive_sides_w_points.json
new file mode 100644
index 00000000000..79a90b0477e
--- /dev/null
+++ b/test/image/mocks/violin_positive_sides_w_points.json
@@ -0,0 +1,35 @@
+{
+    "data": [{
+            "type": "violin",
+            "points": "all",
+            "pointpos": -0.4,
+            "jitter": 0,
+            "x": [0, 5, 7, 8],
+            "side": "positive",
+            "line": {
+                "color": "black"
+            },
+            "fillcolor": "#8dd3c7",
+            "opacity": 0.6,
+            "y0": 0.0
+        }, {
+            "type": "violin",
+            "points": "all",
+            "pointpos": -0.1,
+            "jitter": 0,
+            "x": [20, 25, 27, 28],
+            "side": "positive",
+            "line": {
+                "color": "black"
+            },
+            "fillcolor": "#d3c78d",
+            "opacity": 0.6,
+            "y0": 0.0
+        }],
+    "layout": {
+        "title": "Violins - only positive sided",
+        "legend": {"x": 0},
+        "xaxis": {"zeroline": false},
+        "yaxis": {"dtick": 0.1, "gridcolor": "black"}
+    }
+}
diff --git a/test/jasmine/tests/box_test.js b/test/jasmine/tests/box_test.js
index 5d15f42e0de..abe59d33d4a 100644
--- a/test/jasmine/tests/box_test.js
+++ b/test/jasmine/tests/box_test.js
@@ -240,6 +240,7 @@ describe('Test box hover:', function() {
                 trace.hoveron = 'points';
             });
             fig.layout.hovermode = 'closest';
+            fig.layout.xaxis = {range: [-0.565, 1.5]};
             return fig;
         },
         nums: '(day 1, 0.7)',
@@ -252,6 +253,7 @@ describe('Test box hover:', function() {
                 trace.hoveron = 'points';
             });
             fig.layout.hovermode = 'x';
+            fig.layout.xaxis = {range: [-0.565, 1.5]};
             return fig;
         },
         nums: '0.7',
@@ -265,6 +267,7 @@ describe('Test box hover:', function() {
                 trace.hoveron = 'points+boxes';
             });
             fig.layout.hovermode = 'x';
+            fig.layout.xaxis = {range: [-0.565, 1.5]};
             return fig;
         },
         pos: [215, 200],
@@ -294,6 +297,7 @@ describe('Test box hover:', function() {
                 trace.text = trace.y.map(function(v) { return 'look:' + v; });
             });
             fig.layout.hovermode = 'closest';
+            fig.layout.xaxis = {range: [-0.565, 1.5]};
             return fig;
         },
         nums: '(day 1, 0.7)\nlook:0.7',
@@ -308,6 +312,7 @@ describe('Test box hover:', function() {
                 trace.hoverinfo = 'text';
             });
             fig.layout.hovermode = 'closest';
+            fig.layout.xaxis = {range: [-0.565, 1.5]};
             return fig;
         },
         nums: 'look:0.7',
@@ -449,7 +454,7 @@ describe('Test box restyle:', function() {
             });
         })
         .then(function() {
-            _assert('auto rng / all boxpoints', [-0.695, 0.5], [-0.555, 10.555]);
+            _assert('auto rng / all boxpoints', [-0.5055, 0.5], [-0.555, 10.555]);
             return Plotly.restyle(gd, 'boxpoints', false);
         })
         .then(function() {
@@ -458,4 +463,36 @@ describe('Test box restyle:', function() {
         .catch(failTest)
         .then(done);
     });
+
+    it('should be able to change axis range when the number of distinct positions changes', function(done) {
+        function _assert(msg, xrng, yrng) {
+            var fullLayout = gd._fullLayout;
+            expect(fullLayout.xaxis.range).toBeCloseToArray(xrng, 2, msg + ' xrng');
+            expect(fullLayout.yaxis.range).toBeCloseToArray(yrng, 2, msg + ' yrng');
+        }
+
+        Plotly.plot(gd, [{
+            type: 'box',
+            width: 0.4,
+            y: [0, 5, 7, 8],
+            y0: 0
+        }, {
+            type: 'box',
+            y: [0, 5, 7, 8],
+            y0: 0.1
+        }])
+        .then(function() {
+            _assert('base', [-0.2, 1.5], [-0.444, 8.444]);
+            return Plotly.restyle(gd, 'visible', [true, 'legendonly']);
+        })
+        .then(function() {
+            _assert('only trace0 visible', [-0.2, 0.2], [-0.444, 8.444]);
+            return Plotly.restyle(gd, 'visible', ['legendonly', true]);
+        })
+        .then(function() {
+            _assert('only trace1 visible', [-0.5, 0.5], [-0.444, 8.444]);
+        })
+        .catch(failTest)
+        .then(done);
+    });
 });
diff --git a/test/jasmine/tests/cartesian_interact_test.js b/test/jasmine/tests/cartesian_interact_test.js
index b124e9cc2fb..aeca9bf68a0 100644
--- a/test/jasmine/tests/cartesian_interact_test.js
+++ b/test/jasmine/tests/cartesian_interact_test.js
@@ -77,10 +77,10 @@ describe('main plot pan', function() {
         var mock = require('@mocks/10.json');
         var precision = 5;
 
-        var originalX = [-0.6225, 5.5];
+        var originalX = [-0.5251046025104602, 5.5];
         var originalY = [-1.6340975059013805, 7.166241526218911];
 
-        var newX = [-2.0255729166666665, 4.096927083333333];
+        var newX = [-1.905857740585774, 4.119246861924687];
         var newY = [-0.3769062155984817, 8.42343281652181];
 
         function _drag(x0, y0, x1, y1) {
diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js
index 2a903e20c1c..3f165f7cfc7 100644
--- a/test/jasmine/tests/select_test.js
+++ b/test/jasmine/tests/select_test.js
@@ -2294,6 +2294,7 @@ describe('Test select box and lasso per trace:', function() {
         fig.layout.dragmode = 'lasso';
         fig.layout.width = 600;
         fig.layout.height = 500;
+        fig.layout.xaxis = {range: [-0.565, 1.5]};
         addInvisible(fig);
 
         Plotly.plot(gd, fig)
diff --git a/test/jasmine/tests/violin_test.js b/test/jasmine/tests/violin_test.js
index d4149927eb4..454ceb50c95 100644
--- a/test/jasmine/tests/violin_test.js
+++ b/test/jasmine/tests/violin_test.js
@@ -142,6 +142,23 @@ describe('Test violin defaults', function() {
         expect(traceOut.meanline.color).toBe('blue');
         expect(traceOut.meanline.width).toBe(10);
     });
+
+    it('should not coerce *scalegroup* and *scalemode* when *width* is set', function() {
+        _supply({
+            y: [1, 2, 1],
+            width: 1
+        });
+        expect(traceOut.scalemode).toBeUndefined();
+        expect(traceOut.scalegroup).toBeUndefined();
+
+        _supply({
+            y: [1, 2, 1],
+            // width=0 is ignored during calc
+            width: 0
+        });
+        expect(traceOut.scalemode).toBe('width');
+        expect(traceOut.scalegroup).toBe('');
+    });
 });
 
 describe('Test violin calc:', function() {
@@ -236,7 +253,7 @@ describe('Test violin calc:', function() {
             name: 'one',
             y: [0, 0, 0, 0, 10, 10, 10, 10]
         });
-        expect(fullLayout._violinScaleGroupStats.one.maxWidth).toBeCloseTo(0.055);
+        expect(fullLayout._violinScaleGroupStats.one.maxKDE).toBeCloseTo(0.055);
         expect(fullLayout._violinScaleGroupStats.one.maxCount).toBe(8);
     });
 
@@ -482,7 +499,10 @@ describe('Test violin hover:', function() {
         patch: function(fig) {
             fig.data[0].x = fig.data[0].y;
             delete fig.data[0].y;
-            fig.layout = {hovermode: 'closest'};
+            fig.layout = {
+                hovermode: 'closest',
+                yaxis: {range: [-0.696, 0.5]}
+            };
             return fig;
         },
         pos: [539, 293],
@@ -567,7 +587,7 @@ describe('Test violin hover:', function() {
 
             Plotly.plot(gd, fig).then(function() {
                 mouseEvent('mousemove', 300, 250);
-                assertViolinHoverLine([299.35, 250, 250, 250]);
+                assertViolinHoverLine([277.3609, 250, 80, 250]);
             })
             .catch(failTest)
             .then(done);
@@ -578,7 +598,7 @@ describe('Test violin hover:', function() {
 
             Plotly.plot(gd, fig).then(function() {
                 mouseEvent('mousemove', 200, 250);
-                assertViolinHoverLine([200.65, 250, 250, 250]);
+                assertViolinHoverLine([222.6391, 250, 420, 250]);
             })
             .catch(failTest)
             .then(done);