diff --git a/src/traces/box/attributes.js b/src/traces/box/attributes.js
index 215aa421145..e2f60b39ba1 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/cross_trace_calc.js b/src/traces/box/cross_trace_calc.js
index 9a4aadd8d56..e1b629b8b85 100644
--- a/src/traces/box/cross_trace_calc.js
+++ b/src/traces/box/cross_trace_calc.js
@@ -13,6 +13,11 @@ var Lib = require('../../lib');
 
 var orientations = ['v', 'h'];
 
+
+function getPosition(di) {
+    return di.pos;
+}
+
 function crossTraceCalc(gd, plotinfo) {
     var calcdata = gd.calcdata;
     var xa = plotinfo.xaxis;
@@ -90,22 +95,31 @@ function setPositionOffset(traceType, gd, boxList, posAxis, pad) {
     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
-    });
-
+    // Find maximum trace width
+    // we baseline this at dPos
     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
+        // set the width of this box
+        // override dPos with trace.width if present
+        var thisDPos = calcTrace[0].t.dPos = (calcTrace[0].trace.width / 2) || dPos;
+        var positions = calcTrace.map(getPosition);
+        // 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 trace = calcTrace[0].trace;  // for ease of use
+        var widthMultiplier = (trace.width) ? trace.width : 1;
+        var alt_vpadminus = Math.max(Math.abs(calcTrace[0].trace.pointpos) * padfactor * widthMultiplier * fullLayout[numKey], 0);
+        var alt_vpadplus = Math.max(Math.abs(calcTrace[0].trace.pointpos) * padfactor * widthMultiplier * fullLayout[numKey], 0);
+
+        var side = calcTrace[0].trace.side;
+        var vpadminus = (side === 'positive') ? alt_vpadminus : (thisDPos + pad[0] * padfactor);
+        var vpadplus = (side === 'negative') ? alt_vpadplus : (thisDPos + pad[1] * padfactor);
+        var extremes = Axes.findExtremes(posAxis, positions, {
+            vpadminus: vpadminus,
+            vpadplus: vpadplus
+        });
         calcTrace[0].trace._extremes[posAxis._id] = extremes;
     }
-
 }
 
 module.exports = {
diff --git a/src/traces/box/defaults.js b/src/traces/box/defaults.js
index dc75d7f1266..cdd15dcd035 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 2b75d7f4877..38b3658d264 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 236bfe2b7ea..c362db07ada 100644
--- a/src/traces/box/plot.js
+++ b/src/traces/box/plot.js
@@ -22,8 +22,9 @@ function plot(gd, plotinfo, cdbox, boxLayer) {
     var xa = plotinfo.xaxis;
     var ya = plotinfo.yaxis;
     var numBoxes = fullLayout._numBoxes;
-    var groupFraction = (1 - fullLayout.boxgap);
     var group = (fullLayout.boxmode === 'group' && numBoxes > 1);
+    var groupFraction = (1 - fullLayout.boxgap);
+    var groupGapFraction = 1 - fullLayout.boxgroupgap;
 
     Lib.makeTraceGroups(boxLayer, cdbox, 'trace boxes').each(function(cd) {
         var plotGroup = d3.select(this);
@@ -31,10 +32,22 @@ 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);
+
+        // position coordinate delta
+        var dPos = t.dPos;
+        // box half width;
+        var bdPos;
         // box center offset
-        var bPos = group ? 2 * t.dPos * (-0.5 + (t.num + 0.5) / numBoxes) * groupFraction : 0;
+        var bPos;
+
+        if(trace.width) {
+            bdPos = dPos;
+            bPos = 0;
+        } else {
+            bdPos = dPos * groupFraction * groupGapFraction / (group ? numBoxes : 1);
+            bPos = group ? 2 * dPos * (-0.5 + (t.num + 0.5) / numBoxes) * groupFraction : 0;
+        }
+
         // whisker width
         var wdPos = bdPos * trace.whiskerwidth;
 
diff --git a/src/traces/violin/attributes.js b/src/traces/violin/attributes.js
index 965b8fecbb7..bdf6e3af3dc 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,
 
diff --git a/src/traces/violin/calc.js b/src/traces/violin/calc.js
index 1994f4235c3..52a72e57462 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/defaults.js b/src/traces/violin/defaults.js
index 869e7b08320..3b394518d87 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 b02e309c660..ce6f1f2651a 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 43b53a8046e..de046a5a9b2 100644
--- a/src/traces/violin/plot.js
+++ b/src/traces/violin/plot.js
@@ -20,6 +20,10 @@ module.exports = function plot(gd, plotinfo, cdViolins, violinLayer) {
     var fullLayout = gd._fullLayout;
     var xa = plotinfo.xaxis;
     var ya = plotinfo.yaxis;
+    var numViolins = fullLayout._numViolins;
+    var group = (fullLayout.violinmode === 'group' && numViolins > 1);
+    var groupFraction = 1 - fullLayout.violingap;
+    var groupGapFraction = 1 - fullLayout.violingroupgap;
 
     function makePath(pts) {
         var segments = linePoints(pts, {
@@ -39,16 +43,30 @@ 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;
+
+        // position coordinate delta
+        var dPos = t.dPos;
         // violin max half width
-        var bdPos = t.bdPos = t.dPos * groupFraction * (1 - fullLayout.violingroupgap) / (group ? numViolins : 1);
+        var bdPos;
         // violin center offset
-        var bPos = t.bPos = group ? 2 * t.dPos * (-0.5 + (t.num + 0.5) / numViolins) * groupFraction : 0;
+        var bPos;
         // 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);
+        var wHover;
+
+        if(trace.width) {
+            bdPos = dPos;
+            bPos = 0;
+            wHover = dPos;
+        } else {
+            bdPos = dPos * groupFraction * groupGapFraction / (group ? numViolins : 1);
+            bPos = group ? 2 * dPos * (-0.5 + (t.num + 0.5) / numViolins) * groupFraction : 0;
+            wHover = dPos * (group ? groupFraction / numViolins : 1);
+        }
+
+        t.bdPos = bdPos;
+        t.bPos = bPos;
+        t.wHover = wHover;
 
         if(trace.visible !== true || t.empty) {
             plotGroup.remove();
@@ -60,7 +78,6 @@ module.exports = function plot(gd, plotinfo, cdViolins, violinLayer) {
         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 +93,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/violin_box_multiple_widths.png b/test/image/baselines/violin_box_multiple_widths.png
new file mode 100644
index 00000000000..d84c0ec8dc3
Binary files /dev/null and b/test/image/baselines/violin_box_multiple_widths.png differ
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..f8bb39d0f81
--- /dev/null
+++ b/test/image/mocks/violin_box_multiple_widths.json
@@ -0,0 +1,44 @@
+{
+    "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
+    }],
+    "layout": {
+        "title": "Joyplot - Violin with multiple widths",
+        "legend": {"x": 0},
+        "xaxis": {"zeroline": false},
+        "yaxis": {"dtick": 0.1, "gridcolor": "black"}
+    }
+}
diff --git a/test/jasmine/tests/violin_test.js b/test/jasmine/tests/violin_test.js
index c925b096acb..643f7548e5d 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);
     });
 
@@ -533,7 +550,7 @@ describe('Test violin hover:', function() {
 
             Plotly.plot(gd, fig).then(function() {
                 mouseEvent('mousemove', 300, 250);
-                assertViolinHoverLine([299.35, 250, 250, 250]);
+                assertViolinHoverLine([178.67823028564453, 250, 80, 250]);
             })
             .catch(failTest)
             .then(done);
@@ -544,7 +561,7 @@ describe('Test violin hover:', function() {
 
             Plotly.plot(gd, fig).then(function() {
                 mouseEvent('mousemove', 200, 250);
-                assertViolinHoverLine([200.65, 250, 250, 250]);
+                assertViolinHoverLine([321.3217315673828, 250, 420, 250]);
             })
             .catch(failTest)
             .then(done);