diff --git a/src/components/legend/constants.js b/src/components/legend/constants.js
new file mode 100644
index 00000000000..c36b7defb81
--- /dev/null
+++ b/src/components/legend/constants.js
@@ -0,0 +1,8 @@
+'use strict';
+
+module.exports = {
+    scrollBarWidth: 4,
+    scrollBarHeight: 20,
+    scrollBarColor: '#808BA4',
+    scrollBarMargin: 4
+};
diff --git a/src/components/legend/index.js b/src/components/legend/index.js
index 464774b1d79..058f5c120b8 100644
--- a/src/components/legend/index.js
+++ b/src/components/legend/index.js
@@ -12,11 +12,20 @@
 var Plotly = require('../../plotly');
 var d3 = require('d3');
 
+var Lib = require('../../lib');
+
+var Plots = require('../../plots/plots');
+var Fx = require('../../plots/cartesian/graph_interact');
+
+var Color = require('../color');
+var Drawing = require('../drawing');
+
 var subTypes = require('../../traces/scatter/subtypes');
 var styleOne = require('../../traces/pie/style_one');
 
 var legend = module.exports = {};
 
+var constants = require('./constants');
 legend.layoutAttributes = require('./attributes');
 
 legend.supplyLayoutDefaults = function(layoutIn, layoutOut, fullData) {
@@ -33,10 +42,10 @@ legend.supplyLayoutDefaults = function(layoutIn, layoutOut, fullData) {
         if(legendGetsTrace(trace)) {
             visibleTraces++;
             // always show the legend by default if there's a pie
-            if(Plotly.Plots.traceIs(trace, 'pie')) visibleTraces++;
+            if(Plots.traceIs(trace, 'pie')) visibleTraces++;
         }
 
-        if((Plotly.Plots.traceIs(trace, 'bar') && layoutOut.barmode==='stack') ||
+        if((Plots.traceIs(trace, 'bar') && layoutOut.barmode==='stack') ||
                 ['tonextx','tonexty'].indexOf(trace.fill)!==-1) {
             defaultOrder = isGrouped({traceorder: defaultOrder}) ?
                 'grouped+reversed' : 'reversed';
@@ -49,19 +58,19 @@ legend.supplyLayoutDefaults = function(layoutIn, layoutOut, fullData) {
     }
 
     function coerce(attr, dflt) {
-        return Plotly.Lib.coerce(containerIn, containerOut,
+        return Lib.coerce(containerIn, containerOut,
             legend.layoutAttributes, attr, dflt);
     }
 
-    var showLegend = Plotly.Lib.coerce(layoutIn, layoutOut,
-        Plotly.Plots.layoutAttributes, 'showlegend', visibleTraces > 1);
+    var showLegend = Lib.coerce(layoutIn, layoutOut,
+        Plots.layoutAttributes, 'showlegend', visibleTraces > 1);
 
     if(showLegend === false) return;
 
     coerce('bgcolor', layoutOut.paper_bgcolor);
     coerce('bordercolor');
     coerce('borderwidth');
-    Plotly.Lib.coerceFont(coerce, 'font', layoutOut.font);
+    Lib.coerceFont(coerce, 'font', layoutOut.font);
 
     coerce('traceorder', defaultOrder);
     if(isGrouped(layoutOut.legend)) coerce('tracegroupgap');
@@ -70,7 +79,7 @@ legend.supplyLayoutDefaults = function(layoutIn, layoutOut, fullData) {
     coerce('xanchor');
     coerce('y');
     coerce('yanchor');
-    Plotly.Lib.noneOrAll(containerIn, containerOut, ['x', 'y']);
+    Lib.noneOrAll(containerIn, containerOut, ['x', 'y']);
 };
 
 // -----------------------------------------------------
@@ -88,14 +97,14 @@ legend.lines = function(d){
     fill.enter().append('path').classed('js-fill',true);
     fill.exit().remove();
     fill.attr('d', 'M5,0h30v6h-30z')
-        .call(Plotly.Drawing.fillGroupStyle);
+        .call(Drawing.fillGroupStyle);
 
     var line = d3.select(this).select('.legendlines').selectAll('path')
         .data(showLine ? [d] : []);
     line.enter().append('path').classed('js-line',true)
         .attr('d', 'M5,0h30');
     line.exit().remove();
-    line.call(Plotly.Drawing.lineGroupStyle);
+    line.call(Drawing.lineGroupStyle);
 };
 
 legend.points = function(d){
@@ -111,7 +120,7 @@ legend.points = function(d){
     // use d0.trace to infer arrayOk attributes
 
     function boundVal(attrIn, arrayToValFn, bounds) {
-        var valIn = Plotly.Lib.nestedProperty(trace, attrIn).get(),
+        var valIn = Lib.nestedProperty(trace, attrIn).get(),
             valToBound = (Array.isArray(valIn) && arrayToValFn) ?
                 arrayToValFn(valIn) : valIn;
 
@@ -131,10 +140,10 @@ legend.points = function(d){
 
         if(showMarkers) {
             dEdit.mc = boundVal('marker.color', pickFirst);
-            dEdit.mo = boundVal('marker.opacity', Plotly.Lib.mean, [0.2, 1]);
-            dEdit.ms = boundVal('marker.size', Plotly.Lib.mean, [2, 16]);
+            dEdit.mo = boundVal('marker.opacity', Lib.mean, [0.2, 1]);
+            dEdit.ms = boundVal('marker.size', Lib.mean, [2, 16]);
             dEdit.mlc = boundVal('marker.line.color', pickFirst);
-            dEdit.mlw = boundVal('marker.line.width', Plotly.Lib.mean, [0, 5]);
+            dEdit.mlw = boundVal('marker.line.width', Lib.mean, [0, 5]);
             tEdit.marker = {
                 sizeref: 1,
                 sizemin: 1,
@@ -156,8 +165,8 @@ legend.points = function(d){
             dEdit.tf = boundVal('textfont.family', pickFirst);
         }
 
-        dMod = [Plotly.Lib.minExtend(d0, dEdit)];
-        tMod = Plotly.Lib.minExtend(trace, tEdit);
+        dMod = [Lib.minExtend(d0, dEdit)];
+        tMod = Lib.minExtend(trace, tEdit);
     }
 
     var ptgroup = d3.select(this).select('g.legendpoints');
@@ -167,7 +176,7 @@ legend.points = function(d){
     pts.enter().append('path').classed('scatterpts', true)
         .attr('transform', 'translate(20,0)');
     pts.exit().remove();
-    pts.call(Plotly.Drawing.pointStyle, tMod);
+    pts.call(Drawing.pointStyle, tMod);
 
     // 'mrc' is set in pointStyle and used in textPointStyle:
     // constrain it here
@@ -179,7 +188,7 @@ legend.points = function(d){
         .append('g').classed('pointtext',true)
             .append('text').attr('transform', 'translate(20,0)');
     txt.exit().remove();
-    txt.selectAll('text').call(Plotly.Drawing.textPointStyle, tMod);
+    txt.selectAll('text').call(Drawing.textPointStyle, tMod);
 
 };
 
@@ -189,7 +198,7 @@ legend.bars = function(d){
         markerLine = marker.line||{},
         barpath = d3.select(this).select('g.legendpoints')
             .selectAll('path.legendbar')
-            .data(Plotly.Plots.traceIs(trace, 'bar') ? [d] : []);
+            .data(Plots.traceIs(trace, 'bar') ? [d] : []);
     barpath.enter().append('path').classed('legendbar',true)
         .attr('d','M6,6H-6V-6H6Z')
         .attr('transform','translate(20,0)');
@@ -198,9 +207,9 @@ legend.bars = function(d){
         var w = (d.mlw+1 || markerLine.width+1) - 1,
             p = d3.select(this);
         p.style('stroke-width',w+'px')
-            .call(Plotly.Color.fill, d.mc || marker.color);
+            .call(Color.fill, d.mc || marker.color);
         if(w) {
-            p.call(Plotly.Color.stroke, d.mlc || markerLine.color);
+            p.call(Color.stroke, d.mlc || markerLine.color);
         }
     });
 };
@@ -209,7 +218,7 @@ legend.boxes = function(d){
     var trace = d[0].trace,
         pts = d3.select(this).select('g.legendpoints')
             .selectAll('path.legendbox')
-            .data(Plotly.Plots.traceIs(trace, 'box') && trace.visible ? [d] : []);
+            .data(Plots.traceIs(trace, 'box') && 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')
@@ -219,9 +228,9 @@ legend.boxes = function(d){
         var w = (d.lw+1 || trace.line.width+1) - 1,
             p = d3.select(this);
         p.style('stroke-width', w+'px')
-            .call(Plotly.Color.fill, d.fc || trace.fillcolor);
+            .call(Color.fill, d.fc || trace.fillcolor);
         if(w) {
-            p.call(Plotly.Color.stroke, d.lc || trace.line.color);
+            p.call(Color.stroke, d.lc || trace.line.color);
         }
     });
 };
@@ -230,7 +239,7 @@ legend.pie = function(d) {
     var trace = d[0].trace,
         pts = d3.select(this).select('g.legendpoints')
             .selectAll('path.legendpie')
-            .data(Plotly.Plots.traceIs(trace, 'pie') && trace.visible ? [d] : []);
+            .data(Plots.traceIs(trace, 'pie') && trace.visible ? [d] : []);
     pts.enter().append('path').classed('legendpie', true)
         .attr('d', 'M6,6H-6V-6H6Z')
         .attr('transform', 'translate(20,0)');
@@ -277,7 +286,7 @@ legend.style = function(s) {
 legend.texts = function(context, td, d, i, traces){
     var fullLayout = td._fullLayout,
         trace = d[0].trace,
-        isPie = Plotly.Plots.traceIs(trace, 'pie'),
+        isPie = Plots.traceIs(trace, 'pie'),
         traceIndex = trace.index,
         name = isPie ? d[0].label : trace.name;
 
@@ -286,12 +295,18 @@ legend.texts = function(context, td, d, i, traces){
     text.enter().append('text').classed('legendtext', true);
     text.attr({
         x: 40,
-        y: 0
+        y: 0,
+        'data-unformatted': name
     })
-    .style('text-anchor', 'start')
-    .call(Plotly.Drawing.font, fullLayout.legend.font)
-    .text(name)
-    .attr({'data-unformatted': name});
+    .style({
+        'text-anchor': 'start',
+        '-webkit-user-select': 'none',
+        '-moz-user-select': 'none',
+        '-ms-user-select': 'none',
+        'user-select': 'none'
+    })
+    .call(Drawing.font, fullLayout.legend.font)
+    .text(name);
 
     function textLayout(s){
         Plotly.util.convertToTspans(s, function(){
@@ -319,7 +334,7 @@ legend.texts = function(context, td, d, i, traces){
 // -----------------------------------------------------
 
 function legendGetsTrace(trace) {
-    return trace.visible && Plotly.Plots.traceIs(trace, 'showLegend');
+    return trace.visible && Plots.traceIs(trace, 'showLegend');
 }
 
 function isGrouped(legendLayout) {
@@ -365,7 +380,7 @@ legend.getLegendData = function(calcdata, opts) {
 
         if(!legendGetsTrace(trace) || !trace.showlegend) continue;
 
-        if(Plotly.Plots.traceIs(trace, 'pie')) {
+        if(Plots.traceIs(trace, 'pie')) {
             if(!slicesShown[lgroup]) slicesShown[lgroup] = {};
             for(j = 0; j < cd.length; j++) {
                 labelj = cd[j].label;
@@ -426,7 +441,7 @@ legend.draw = function(td) {
 
     if(!fullLayout.showlegend || !legendData.length) {
         fullLayout._infolayer.selectAll('.legend').remove();
-        Plotly.Plots.autoMargin(td, 'legend');
+        Plots.autoMargin(td, 'legend');
         return;
     }
 
@@ -435,22 +450,43 @@ legend.draw = function(td) {
 
     var legendsvg = fullLayout._infolayer.selectAll('svg.legend')
         .data([0]);
-    legendsvg.enter(0).append('svg')
-        .attr('class','legend');
+    legendsvg.enter().append('svg')
+        .attr({
+            'class': 'legend',
+            'pointer-events': 'all'
+        });
 
-    var bgRect = legendsvg.selectAll('rect.bg')
+    var bg = legendsvg.selectAll('rect.bg')
         .data([0]);
-    bgRect.enter(0).append('rect')
-        .attr('class','bg');
-    bgRect
-        .call(Plotly.Color.stroke, opts.bordercolor)
-        .call(Plotly.Color.fill, opts.bgcolor)
-        .style('stroke-width', opts.borderwidth+'px');
-
-    var groups = legendsvg.selectAll('g.groups')
-        .data(legendData);
+    bg.enter().append('rect')
+        .attr({
+            'class': 'bg',
+            'shape-rendering': 'crispEdges'
+        })
+        .call(Color.stroke, opts.bordercolor)
+        .call(Color.fill, opts.bgcolor)
+        .style('stroke-width', opts.borderwidth + 'px');
+
+    var scrollBox = legendsvg.selectAll('g.scrollbox')
+        .data([0]);
+    scrollBox.enter().append('g')
+        .attr('class', 'scrollbox');
+    scrollBox.exit().remove();
+
+    var scrollBar = legendsvg.selectAll('rect.scrollbar')
+        .data([0]);
+    scrollBar.enter().append('rect')
+        .attr({
+            'class': 'scrollbar',
+            'rx': 20,
+            'ry': 2
+        })
+        .call(Color.fill, '#808BA4');
 
-    groups.enter().append('g').attr('class', 'groups');
+    var groups = scrollBox.selectAll('g.groups')
+        .data(legendData);
+    groups.enter().append('g')
+        .attr('class', 'groups');
     groups.exit().remove();
 
     if(isGrouped(opts)) {
@@ -460,7 +496,7 @@ legend.draw = function(td) {
     }
 
     var traces = groups.selectAll('g.traces')
-        .data(Plotly.Lib.identity);
+        .data(Lib.identity);
 
     traces.enter().append('g').attr('class', 'traces');
     traces.exit().remove();
@@ -468,7 +504,7 @@ legend.draw = function(td) {
     traces.call(legend.style)
         .style('opacity', function(d) {
             var trace = d[0].trace;
-            if(Plotly.Plots.traceIs(trace, 'pie')) {
+            if(Plots.traceIs(trace, 'pie')) {
                 return hiddenSlices.indexOf(d[0].label) !== -1 ? 0.5 : 1;
             } else {
                 return trace.visible === 'legendonly' ? 0.5 : 1;
@@ -483,7 +519,7 @@ legend.draw = function(td) {
                 .classed('legendtoggle', true)
                 .style('cursor', 'pointer')
                 .attr('pointer-events', 'all')
-                .call(Plotly.Color.fill, 'rgba(0,0,0,0)');
+                .call(Color.fill, 'rgba(0,0,0,0)');
             traceToggle.on('click', function() {
                 if(td._dragged) return;
 
@@ -494,7 +530,7 @@ legend.draw = function(td) {
                     tracei,
                     newVisible;
 
-                if(Plotly.Plots.traceIs(trace, 'pie')) {
+                if(Plots.traceIs(trace, 'pie')) {
                     var thisLabel = d[0].label,
                         newHiddenSlices = hiddenSlices.slice(),
                         thisLabelIndex = newHiddenSlices.indexOf(thisLabel);
@@ -521,8 +557,101 @@ legend.draw = function(td) {
             });
         });
 
+    // Position and size the legend
     legend.repositionLegend(td, traces);
 
+    // Scroll section must be executed after repositionLegend.
+    // It requires the legend width, height, x and y to position the scrollbox
+    // and these values are mutated in repositionLegend.
+    var gs = fullLayout._size,
+        lx = gs.l + gs.w * opts.x,
+        ly = gs.t + gs.h * (1-opts.y);
+
+    if(opts.xanchor === 'right' || (opts.xanchor === 'auto' && opts.x >= 2 / 3)) {
+        lx -= opts.width;
+    }
+    else if(opts.xanchor === 'center' || (opts.xanchor === 'auto' && opts.x > 1 / 3)) {
+        lx -= opts.width / 2;
+    }
+
+    if(opts.yanchor === 'bottom' || (opts.yanchor === 'auto' && opts.y <= 1 / 3)) {
+        ly -= opts.height;
+    }
+    else if(opts.yanchor === 'middle' || (opts.yanchor === 'auto' && opts.y < 2 / 3)) {
+        ly -= opts.height / 2;
+    }
+
+    // Deal with scrolling
+    var plotHeight = fullLayout.height - fullLayout.margin.b,
+        scrollheight = Math.min(plotHeight - ly, opts.height),
+        scrollPosition = scrollBox.attr('data-scroll') ? scrollBox.attr('data-scroll') : 0;
+
+    scrollBox.attr('transform', 'translate(0, ' + scrollPosition + ')');
+    bg.attr({
+        width: opts.width - 2 * opts.borderwidth,
+        height: scrollheight - 2 * opts.borderwidth,
+        x: opts.borderwidth,
+        y: opts.borderwidth
+    });
+
+    legendsvg.call(Drawing.setRect, lx, ly, opts.width, scrollheight);
+
+    // If scrollbar should be shown.
+    if(td.firstRender && opts.height - scrollheight > 0 && !td._context.staticPlot){
+
+        bg.attr({ width: opts.width - 2 * opts.borderwidth + constants.scrollBarWidth });
+
+        legendsvg.node().addEventListener('wheel', function(e){
+            e.preventDefault();
+            scrollHandler(e.deltaY / 20);
+        });
+
+        scrollBar.node().addEventListener('mousedown', function(e) {
+            e.preventDefault();
+
+            function mMove(e) {
+                if(e.buttons === 1){
+                    scrollHandler(e.movementY);
+                }
+            }
+
+            function mUp() {
+                scrollBar.node().removeEventListener('mousemove', mMove);
+                window.removeEventListener('mouseup', mUp);
+            }
+
+            window.addEventListener('mousemove', mMove);
+            window.addEventListener('mouseup', mUp);
+        });
+
+            // Move scrollbar to starting position on the first render
+        scrollBar.call(
+            Drawing.setRect,
+            opts.width - (constants.scrollBarWidth + constants.scrollBarMargin),
+            constants.scrollBarMargin,
+            constants.scrollBarWidth,
+            constants.scrollBarHeight
+        );
+    }
+
+    function scrollHandler(delta){
+
+        var scrollBarTrack = scrollheight - constants.scrollBarHeight - 2 * constants.scrollBarMargin,
+            translateY = scrollBox.attr('data-scroll'),
+            scrollBoxY = Lib.constrain(translateY - delta, Math.min(scrollheight - opts.height, 0), 0),
+            scrollBarY = -scrollBoxY / (opts.height - scrollheight) * scrollBarTrack + constants.scrollBarMargin;
+
+        scrollBox.attr('data-scroll', scrollBoxY);
+        scrollBox.attr('transform', 'translate(0, ' + scrollBoxY + ')');
+        scrollBar.call(
+            Drawing.setRect,
+            opts.width - (constants.scrollBarWidth + constants.scrollBarMargin),
+            scrollBarY,
+            constants.scrollBarWidth,
+            constants.scrollBarHeight
+        );
+    }
+
     if(td._context.editable) {
         var xf,
             yf,
@@ -531,32 +660,32 @@ legend.draw = function(td) {
             lw,
             lh;
 
-        Plotly.Fx.dragElement({
+        Fx.dragElement({
             element: legendsvg.node(),
             prepFn: function() {
                 x0 = Number(legendsvg.attr('x'));
                 y0 = Number(legendsvg.attr('y'));
                 lw = Number(legendsvg.attr('width'));
                 lh = Number(legendsvg.attr('height'));
-                Plotly.Fx.setCursor(legendsvg);
+                Fx.setCursor(legendsvg);
             },
             moveFn: function(dx, dy) {
                 var gs = td._fullLayout._size;
 
-                legendsvg.call(Plotly.Drawing.setPosition, x0+dx, y0+dy);
+                legendsvg.call(Drawing.setPosition, x0+dx, y0+dy);
 
-                xf = Plotly.Fx.dragAlign(x0+dx, lw, gs.l, gs.l+gs.w,
+                xf = Fx.dragAlign(x0+dx, lw, gs.l, gs.l+gs.w,
                     opts.xanchor);
-                yf = Plotly.Fx.dragAlign(y0+dy+lh, -lh, gs.t+gs.h, gs.t,
+                yf = Fx.dragAlign(y0+dy+lh, -lh, gs.t+gs.h, gs.t,
                     opts.yanchor);
 
-                var csr = Plotly.Fx.dragCursors(xf, yf,
+                var csr = Fx.dragCursors(xf, yf,
                     opts.xanchor, opts.yanchor);
-                Plotly.Fx.setCursor(legendsvg, csr);
+                Fx.setCursor(legendsvg, csr);
             },
             doneFn: function(dragged) {
-                Plotly.Fx.setCursor(legendsvg);
-                if(dragged && xf!==undefined && yf!==undefined) {
+                Fx.setCursor(legendsvg);
+                if(dragged && xf !== undefined && yf !== undefined) {
                     Plotly.relayout(td, {'legend.x': xf, 'legend.y': yf});
                 }
             }
@@ -568,12 +697,10 @@ legend.repositionLegend = function(td, traces){
     var fullLayout = td._fullLayout,
         gs = fullLayout._size,
         opts = fullLayout.legend,
-        borderwidth = opts.borderwidth,
+        borderwidth = opts.borderwidth;
 
-        // add the legend elements, keeping track of the
-        // legend size (in px) as we go
-        legendwidth = 0,
-        legendheight = 0;
+    opts.width = 0,
+    opts.height = 0,
 
     traces.each(function(d){
         var trace = d[0].trace,
@@ -582,8 +709,8 @@ legend.repositionLegend = function(td, traces){
             text = g.selectAll('.legendtext'),
             tspans = g.selectAll('.legendtext>tspan'),
             tHeight = opts.font.size * 1.3,
-            tLines = tspans[0].length||1,
-            tWidth = text.node() && Plotly.Drawing.bBox(text.node()).width,
+            tLines = tspans[0].length || 1,
+            tWidth = text.node() && Drawing.bBox(text.node()).width,
             mathjaxGroup = g.select('g[class*=math-group]'),
             textY,
             tHeightFull;
@@ -594,15 +721,15 @@ legend.repositionLegend = function(td, traces){
         }
 
         if(mathjaxGroup.node()) {
-            var mathjaxBB = Plotly.Drawing.bBox(mathjaxGroup.node());
+            var mathjaxBB = Drawing.bBox(mathjaxGroup.node());
             tHeight = mathjaxBB.height;
             tWidth = mathjaxBB.width;
-            mathjaxGroup.attr('transform','translate(0,'+(tHeight/4)+')');
+            mathjaxGroup.attr('transform','translate(0,' + (tHeight / 4) + ')');
         }
         else {
             // approximation to height offset to center the font
             // to avoid getBoundingClientRect
-            textY = tHeight * (0.3 + (1-tLines)/2);
+            textY = tHeight * (0.3 + (1-tLines) / 2);
             text.attr('y',textY);
             tspans.attr('y',textY);
         }
@@ -611,22 +738,23 @@ legend.repositionLegend = function(td, traces){
 
         g.attr('transform',
             'translate(' + borderwidth + ',' +
-                (5 + borderwidth + legendheight + tHeightFull/2) +
+                (5 + borderwidth + opts.height + tHeightFull / 2) +
             ')'
         );
         bg.attr({x: 0, y: -tHeightFull / 2, height: tHeightFull});
 
-        legendheight += tHeightFull;
-        legendwidth = Math.max(legendwidth, tWidth||0);
+        opts.height += tHeightFull;
+        opts.width = Math.max(opts.width, tWidth || 0);
     });
 
-    if(isGrouped(opts)) legendheight += (opts._lgroupsLength-1) * opts.tracegroupgap;
 
-    traces.selectAll('.legendtoggle')
-        .attr('width', (td._context.editable ? 0 : legendwidth) + 40);
+    opts.width += 45 + borderwidth * 2;
+    opts.height += 10 + borderwidth * 2;
 
-    legendwidth += 45+borderwidth*2;
-    legendheight += 10+borderwidth*2;
+    if(isGrouped(opts)) opts.height += (opts._lgroupsLength-1) * opts.tracegroupgap;
+
+    traces.selectAll('.legendtoggle')
+        .attr('width', (td._context.editable ? 0 : opts.width) + 40);
 
     // now position the legend. for both x,y the positions are recorded as
     // fractions of the plot area (left, bottom = 0,0). Outside the plot
@@ -634,48 +762,42 @@ legend.repositionLegend = function(td, traces){
     // values <1/3 align the low side at that fraction, 1/3-2/3 align the
     // center at that fraction, >2/3 align the right at that fraction
 
-    var lx = gs.l+gs.w*opts.x,
-        ly = gs.t+gs.h*(1-opts.y);
+    var lx = gs.l + gs.w * opts.x,
+        ly = gs.t + gs.h * (1-opts.y);
 
     var xanchor = 'left';
-    if(opts.xanchor==='right' || (opts.xanchor==='auto' && opts.x>=2/3)) {
-        lx -= legendwidth;
+    if(opts.xanchor === 'right' || (opts.xanchor === 'auto' && opts.x >= 2 / 3)) {
+        lx -= opts.width;
         xanchor = 'right';
     }
-    else if(opts.xanchor==='center' || (opts.xanchor==='auto' && opts.x>1/3)) {
-        lx -= legendwidth/2;
+    else if(opts.xanchor === 'center' || (opts.xanchor === 'auto' && opts.x > 1 / 3)) {
+        lx -= opts.width / 2;
         xanchor = 'center';
     }
 
     var yanchor = 'top';
-    if(opts.yanchor==='bottom' || (opts.yanchor==='auto' && opts.y<=1/3)) {
-        ly -= legendheight;
+    if(opts.yanchor === 'bottom' || (opts.yanchor === 'auto' && opts.y <= 1 / 3)) {
+        ly -= opts.height;
         yanchor = 'bottom';
     }
-    else if(opts.yanchor==='middle' || (opts.yanchor==='auto' && opts.y<2/3)) {
-        ly -= legendheight/2;
+    else if(opts.yanchor === 'middle' || (opts.yanchor === 'auto' && opts.y < 2 / 3)) {
+        ly -= opts.height / 2;
         yanchor = 'middle';
     }
 
     // make sure we're only getting full pixels
-    legendwidth = Math.ceil(legendwidth);
-    legendheight = Math.ceil(legendheight);
+    opts.width = Math.ceil(opts.width);
+    opts.height = Math.ceil(opts.height);
     lx = Math.round(lx);
     ly = Math.round(ly);
 
-    fullLayout._infolayer.selectAll('svg.legend')
-        .call(Plotly.Drawing.setRect, lx, ly, legendwidth, legendheight);
-    fullLayout._infolayer.selectAll('svg.legend .bg')
-        .call(Plotly.Drawing.setRect, borderwidth/2, borderwidth/2,
-            legendwidth-borderwidth, legendheight-borderwidth);
-
     // lastly check if the margin auto-expand has changed
-    Plotly.Plots.autoMargin(td,'legend',{
+    Plots.autoMargin(td,'legend',{
         x: opts.x,
         y: opts.y,
-        l: legendwidth * ({right:1, center:0.5}[xanchor]||0),
-        r: legendwidth * ({left:1, center:0.5}[xanchor]||0),
-        b: legendheight * ({top:1, middle:0.5}[yanchor]||0),
-        t: legendheight * ({bottom:1, middle:0.5}[yanchor]||0)
+        l: opts.width * ({right:1, center:0.5}[xanchor] || 0),
+        r: opts.width * ({left:1, center:0.5}[xanchor] || 0),
+        b: opts.height * ({top:1, middle:0.5}[yanchor] || 0),
+        t: opts.height * ({bottom:1, middle:0.5}[yanchor] || 0)
     });
 };
diff --git a/src/lib/svg_text_utils.js b/src/lib/svg_text_utils.js
index 032b01fee14..d8b246d7318 100644
--- a/src/lib/svg_text_utils.js
+++ b/src/lib/svg_text_utils.js
@@ -80,7 +80,6 @@ util.convertToTspans = function(_context, _callback){
     parent.selectAll('svg.' + svgClass).remove();
     parent.selectAll('g.' + svgClass + '-group').remove();
     _context.style({visibility: null});
-    // for Plotly.Drawing.bBox: unlink text and all parents from its cached box
     for(var up = _context.node(); up && up.removeAttribute; up = up.parentNode) {
         up.removeAttribute('data-bb');
     }
diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js
index d37e78635bd..f1ab41db719 100644
--- a/src/plot_api/plot_api.js
+++ b/src/plot_api/plot_api.js
@@ -292,13 +292,24 @@ Plotly.plot = function(gd, data, layout, config) {
         // source links
         Plots.addLinks(gd);
 
+        // Mark the first render as complete
+        gd._replotting = false;
+
         return Plots.previousPromises(gd);
     }
 
+    // An initial paint must be completed before these components can be
+    // correctly sized and the whole plot re-margined. gd._replotting must
+    // be set to false before these will work properly.
+    function finalDraw(){
+        Shapes.drawAll(gd);
+        Plotly.Annotations.drawAll(gd);
+        Legend.draw(gd);
+    }
+
     function cleanUp() {
         // now we're REALLY TRULY done plotting...
         // so mark it as done and let other procedures call a replot
-        gd._replotting = false;
         Lib.markTime('done plot');
         gd.emit('plotly_afterplot');
     }
@@ -310,7 +321,8 @@ Plotly.plot = function(gd, data, layout, config) {
         marginPushersAgain,
         positionAndAutorange,
         drawAxes,
-        drawData
+        drawData,
+        finalDraw
     ], gd, cleanUp);
 
     // even if everything we did was synchronous, return a promise
diff --git a/src/snapshot/index.js b/src/snapshot/index.js
index fcf209fb5d3..2b662846928 100644
--- a/src/snapshot/index.js
+++ b/src/snapshot/index.js
@@ -9,8 +9,6 @@
 
 'use strict';
 
-var Plotly = require('../plotly');
-
 function getDelay(fullLayout) {
     return (fullLayout._hasGL3D || fullLayout._hasGL2D) ? 500 : 0;
 }
@@ -24,8 +22,6 @@ function getRedrawFunc(gd) {
             (gd.data && gd.data[0] && gd.data[0].r)
         ) return;
 
-        Plotly.Annotations.drawAll(gd);
-        Plotly.Legend.draw(gd, fullLayout.showlegend);
         (gd.calcdata || []).forEach(function(d) {
             if(d[0] && d[0].t && d[0].t.cb) d[0].t.cb();
         });
diff --git a/test/image/baselines/0.png b/test/image/baselines/0.png
index 5133fb33e13..5a839d4ac52 100644
Binary files a/test/image/baselines/0.png and b/test/image/baselines/0.png differ
diff --git a/test/image/baselines/11.png b/test/image/baselines/11.png
index ed50cb35c92..998509dd8d5 100644
Binary files a/test/image/baselines/11.png and b/test/image/baselines/11.png differ
diff --git a/test/image/baselines/15.png b/test/image/baselines/15.png
index 277d718a46d..d3e08275fce 100644
Binary files a/test/image/baselines/15.png and b/test/image/baselines/15.png differ
diff --git a/test/image/baselines/17.png b/test/image/baselines/17.png
index 1e098f1a02c..98e19ab95ac 100644
Binary files a/test/image/baselines/17.png and b/test/image/baselines/17.png differ
diff --git a/test/image/baselines/19.png b/test/image/baselines/19.png
index 57bb753c15f..3d0619e7d0c 100644
Binary files a/test/image/baselines/19.png and b/test/image/baselines/19.png differ
diff --git a/test/image/baselines/20.png b/test/image/baselines/20.png
index ebc003d849f..5c372badb0c 100644
Binary files a/test/image/baselines/20.png and b/test/image/baselines/20.png differ
diff --git a/test/image/baselines/23.png b/test/image/baselines/23.png
index 7d0c1c793bb..cf24f84cb93 100644
Binary files a/test/image/baselines/23.png and b/test/image/baselines/23.png differ
diff --git a/test/image/baselines/24.png b/test/image/baselines/24.png
index 532d9b6a055..1609cfe305b 100644
Binary files a/test/image/baselines/24.png and b/test/image/baselines/24.png differ
diff --git a/test/image/baselines/27.png b/test/image/baselines/27.png
index 7eb41cc8ac9..be45068e67d 100644
Binary files a/test/image/baselines/27.png and b/test/image/baselines/27.png differ
diff --git a/test/image/baselines/28.png b/test/image/baselines/28.png
index 594cce0b7bd..bcd2b96b1f1 100644
Binary files a/test/image/baselines/28.png and b/test/image/baselines/28.png differ
diff --git a/test/image/baselines/29.png b/test/image/baselines/29.png
index a90c21934ba..a5bed7f5221 100644
Binary files a/test/image/baselines/29.png and b/test/image/baselines/29.png differ
diff --git a/test/image/baselines/5.png b/test/image/baselines/5.png
index fa5d18564fa..e0538488ab2 100644
Binary files a/test/image/baselines/5.png and b/test/image/baselines/5.png differ
diff --git a/test/image/baselines/autorange-tozero-rangemode.png b/test/image/baselines/autorange-tozero-rangemode.png
index cb57b380158..7c4d3d477e0 100644
Binary files a/test/image/baselines/autorange-tozero-rangemode.png and b/test/image/baselines/autorange-tozero-rangemode.png differ
diff --git a/test/image/baselines/axes-autotype-empty.png b/test/image/baselines/axes-autotype-empty.png
index 803acf873c2..55d965bd247 100644
Binary files a/test/image/baselines/axes-autotype-empty.png and b/test/image/baselines/axes-autotype-empty.png differ
diff --git a/test/image/baselines/axes-ticks.png b/test/image/baselines/axes-ticks.png
index 2f53bf1ab3e..ac908438243 100644
Binary files a/test/image/baselines/axes-ticks.png and b/test/image/baselines/axes-ticks.png differ
diff --git a/test/image/baselines/axes_booleans.png b/test/image/baselines/axes_booleans.png
index b4969f1ab8e..01cfaaa0db7 100644
Binary files a/test/image/baselines/axes_booleans.png and b/test/image/baselines/axes_booleans.png differ
diff --git a/test/image/baselines/axes_labels.png b/test/image/baselines/axes_labels.png
index e0d5d95c920..58917494cb1 100644
Binary files a/test/image/baselines/axes_labels.png and b/test/image/baselines/axes_labels.png differ
diff --git a/test/image/baselines/axes_lines.png b/test/image/baselines/axes_lines.png
index 75302915254..ac009453593 100644
Binary files a/test/image/baselines/axes_lines.png and b/test/image/baselines/axes_lines.png differ
diff --git a/test/image/baselines/axes_range_manual.png b/test/image/baselines/axes_range_manual.png
index a53988fba67..60540ad468b 100644
Binary files a/test/image/baselines/axes_range_manual.png and b/test/image/baselines/axes_range_manual.png differ
diff --git a/test/image/baselines/axes_range_type.png b/test/image/baselines/axes_range_type.png
index 7f15a3325a4..af39babcf82 100644
Binary files a/test/image/baselines/axes_range_type.png and b/test/image/baselines/axes_range_type.png differ
diff --git a/test/image/baselines/bar_group_percent.png b/test/image/baselines/bar_group_percent.png
index 45e909bbb0a..c0ccc7f9c13 100644
Binary files a/test/image/baselines/bar_group_percent.png and b/test/image/baselines/bar_group_percent.png differ
diff --git a/test/image/baselines/bar_line.png b/test/image/baselines/bar_line.png
index 3d880ccccbf..5d922d1ba0a 100644
Binary files a/test/image/baselines/bar_line.png and b/test/image/baselines/bar_line.png differ
diff --git a/test/image/baselines/bar_stackto1.png b/test/image/baselines/bar_stackto1.png
index a2a0b38089c..c4ca8cddfa9 100644
Binary files a/test/image/baselines/bar_stackto1.png and b/test/image/baselines/bar_stackto1.png differ
diff --git a/test/image/baselines/bar_stackto100_negative.png b/test/image/baselines/bar_stackto100_negative.png
index 86300d8c214..17fb79bd290 100644
Binary files a/test/image/baselines/bar_stackto100_negative.png and b/test/image/baselines/bar_stackto100_negative.png differ
diff --git a/test/image/baselines/basic_area.png b/test/image/baselines/basic_area.png
index 3707ed0b84f..3cc95b67fbb 100644
Binary files a/test/image/baselines/basic_area.png and b/test/image/baselines/basic_area.png differ
diff --git a/test/image/baselines/basic_line.png b/test/image/baselines/basic_line.png
index 186174010cb..70c712c3b3e 100644
Binary files a/test/image/baselines/basic_line.png and b/test/image/baselines/basic_line.png differ
diff --git a/test/image/baselines/benchmarks.png b/test/image/baselines/benchmarks.png
index 4d4cd85983f..d28ade61946 100644
Binary files a/test/image/baselines/benchmarks.png and b/test/image/baselines/benchmarks.png differ
diff --git a/test/image/baselines/box_grouped.png b/test/image/baselines/box_grouped.png
index 25a088e51b6..291bc99a09a 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 64c959bfbe2..b2f67768d6b 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/bubble_markersize0.png b/test/image/baselines/bubble_markersize0.png
index 5d88235242f..3423fa0c21d 100644
Binary files a/test/image/baselines/bubble_markersize0.png and b/test/image/baselines/bubble_markersize0.png differ
diff --git a/test/image/baselines/bubble_nonnumeric-sizes.png b/test/image/baselines/bubble_nonnumeric-sizes.png
index 7c15676b2c5..e586fa1fd2c 100644
Binary files a/test/image/baselines/bubble_nonnumeric-sizes.png and b/test/image/baselines/bubble_nonnumeric-sizes.png differ
diff --git a/test/image/baselines/custom_size_subplot.png b/test/image/baselines/custom_size_subplot.png
index 6e080840bd1..e8fcddc8aef 100644
Binary files a/test/image/baselines/custom_size_subplot.png and b/test/image/baselines/custom_size_subplot.png differ
diff --git a/test/image/baselines/error_bar_bar.png b/test/image/baselines/error_bar_bar.png
index 5fb87aca20d..3ac2a1ebb2d 100644
Binary files a/test/image/baselines/error_bar_bar.png and b/test/image/baselines/error_bar_bar.png differ
diff --git a/test/image/baselines/error_bar_style.png b/test/image/baselines/error_bar_style.png
index a93648a3237..28f5d931c2f 100644
Binary files a/test/image/baselines/error_bar_style.png and b/test/image/baselines/error_bar_style.png differ
diff --git a/test/image/baselines/geo_africa-insets.png b/test/image/baselines/geo_africa-insets.png
index b23c20e778e..8af3269d1d5 100644
Binary files a/test/image/baselines/geo_africa-insets.png and b/test/image/baselines/geo_africa-insets.png differ
diff --git a/test/image/baselines/geo_bg-color.png b/test/image/baselines/geo_bg-color.png
index 016984c72d7..177b51ccddd 100644
Binary files a/test/image/baselines/geo_bg-color.png and b/test/image/baselines/geo_bg-color.png differ
diff --git a/test/image/baselines/geo_bubbles-colorscales.png b/test/image/baselines/geo_bubbles-colorscales.png
index 5c2c251addd..57c73a839bc 100644
Binary files a/test/image/baselines/geo_bubbles-colorscales.png and b/test/image/baselines/geo_bubbles-colorscales.png differ
diff --git a/test/image/baselines/geo_bubbles-sizeref.png b/test/image/baselines/geo_bubbles-sizeref.png
index 43d02b1f095..8a98a5b2f03 100644
Binary files a/test/image/baselines/geo_bubbles-sizeref.png and b/test/image/baselines/geo_bubbles-sizeref.png differ
diff --git a/test/image/baselines/geo_legendonly.png b/test/image/baselines/geo_legendonly.png
index caa3a04d6f2..0d74a4a40ba 100644
Binary files a/test/image/baselines/geo_legendonly.png and b/test/image/baselines/geo_legendonly.png differ
diff --git a/test/image/baselines/gl3d_log-axis-big.png b/test/image/baselines/gl3d_log-axis-big.png
index 7c65fdbbd4b..7ebf8f9bac6 100644
Binary files a/test/image/baselines/gl3d_log-axis-big.png and b/test/image/baselines/gl3d_log-axis-big.png differ
diff --git a/test/image/baselines/gl3d_log-axis.png b/test/image/baselines/gl3d_log-axis.png
index c2230fb006e..320c3ef1234 100644
Binary files a/test/image/baselines/gl3d_log-axis.png and b/test/image/baselines/gl3d_log-axis.png differ
diff --git a/test/image/baselines/gl3d_multi-scene.png b/test/image/baselines/gl3d_multi-scene.png
index 889fb3939f2..60c16a28cfb 100644
Binary files a/test/image/baselines/gl3d_multi-scene.png and b/test/image/baselines/gl3d_multi-scene.png differ
diff --git a/test/image/baselines/gl3d_opacity-scaling-spikes.png b/test/image/baselines/gl3d_opacity-scaling-spikes.png
index 6f87d3439b8..433d49e74e1 100644
Binary files a/test/image/baselines/gl3d_opacity-scaling-spikes.png and b/test/image/baselines/gl3d_opacity-scaling-spikes.png differ
diff --git a/test/image/baselines/gl3d_projection-traces.png b/test/image/baselines/gl3d_projection-traces.png
index a6057638200..49470a45b25 100644
Binary files a/test/image/baselines/gl3d_projection-traces.png and b/test/image/baselines/gl3d_projection-traces.png differ
diff --git a/test/image/baselines/grouped_bar.png b/test/image/baselines/grouped_bar.png
index 97ae2ffeff4..bdc4cbb1c10 100644
Binary files a/test/image/baselines/grouped_bar.png and b/test/image/baselines/grouped_bar.png differ
diff --git a/test/image/baselines/hist_grouped.png b/test/image/baselines/hist_grouped.png
index 4336d4852fd..9ecbaaf117d 100644
Binary files a/test/image/baselines/hist_grouped.png and b/test/image/baselines/hist_grouped.png differ
diff --git a/test/image/baselines/hist_stacked.png b/test/image/baselines/hist_stacked.png
index ff9f3640b36..a7763b8926f 100644
Binary files a/test/image/baselines/hist_stacked.png and b/test/image/baselines/hist_stacked.png differ
diff --git a/test/image/baselines/legend_inside.png b/test/image/baselines/legend_inside.png
index 85ba1630455..43e346b98ef 100644
Binary files a/test/image/baselines/legend_inside.png and b/test/image/baselines/legend_inside.png differ
diff --git a/test/image/baselines/legend_labels.png b/test/image/baselines/legend_labels.png
index cf7e9d9622a..6b9cbd995e7 100644
Binary files a/test/image/baselines/legend_labels.png and b/test/image/baselines/legend_labels.png differ
diff --git a/test/image/baselines/legend_scroll.png b/test/image/baselines/legend_scroll.png
new file mode 100644
index 00000000000..b26cd5db0bc
Binary files /dev/null and b/test/image/baselines/legend_scroll.png differ
diff --git a/test/image/baselines/legend_style.png b/test/image/baselines/legend_style.png
index 8e6ba7e52fa..2df126e26ce 100644
Binary files a/test/image/baselines/legend_style.png and b/test/image/baselines/legend_style.png differ
diff --git a/test/image/baselines/legend_visibility.png b/test/image/baselines/legend_visibility.png
index 77624f77710..860bc716f06 100644
Binary files a/test/image/baselines/legend_visibility.png and b/test/image/baselines/legend_visibility.png differ
diff --git a/test/image/baselines/legendgroup.png b/test/image/baselines/legendgroup.png
index 97a2f63c932..9ebdd53fa54 100644
Binary files a/test/image/baselines/legendgroup.png and b/test/image/baselines/legendgroup.png differ
diff --git a/test/image/baselines/legendgroup_bar-stack.png b/test/image/baselines/legendgroup_bar-stack.png
index 2d1012b4700..1c205b1d7b4 100644
Binary files a/test/image/baselines/legendgroup_bar-stack.png and b/test/image/baselines/legendgroup_bar-stack.png differ
diff --git a/test/image/baselines/line_scatter.png b/test/image/baselines/line_scatter.png
index c981f8d8bbf..1791dccb1e7 100644
Binary files a/test/image/baselines/line_scatter.png and b/test/image/baselines/line_scatter.png differ
diff --git a/test/image/baselines/line_style.png b/test/image/baselines/line_style.png
index 0b54d003cd3..5b7949b5d05 100644
Binary files a/test/image/baselines/line_style.png and b/test/image/baselines/line_style.png differ
diff --git a/test/image/baselines/multiple_axes_double.png b/test/image/baselines/multiple_axes_double.png
index 135477b5cc7..9185bb90df9 100644
Binary files a/test/image/baselines/multiple_axes_double.png and b/test/image/baselines/multiple_axes_double.png differ
diff --git a/test/image/baselines/multiple_axes_multiple.png b/test/image/baselines/multiple_axes_multiple.png
index b48e0804996..7bdf1630883 100644
Binary files a/test/image/baselines/multiple_axes_multiple.png and b/test/image/baselines/multiple_axes_multiple.png differ
diff --git a/test/image/baselines/multiple_subplots.png b/test/image/baselines/multiple_subplots.png
index 1920139c013..465f7c634b0 100644
Binary files a/test/image/baselines/multiple_subplots.png and b/test/image/baselines/multiple_subplots.png differ
diff --git a/test/image/baselines/pie_fonts.png b/test/image/baselines/pie_fonts.png
index 618227f31fa..1a58140d2f7 100644
Binary files a/test/image/baselines/pie_fonts.png and b/test/image/baselines/pie_fonts.png differ
diff --git a/test/image/baselines/pie_label0_dlabel.png b/test/image/baselines/pie_label0_dlabel.png
index 0feef556c38..c894b985d75 100644
Binary files a/test/image/baselines/pie_label0_dlabel.png and b/test/image/baselines/pie_label0_dlabel.png differ
diff --git a/test/image/baselines/pie_scale_textpos_hideslices.png b/test/image/baselines/pie_scale_textpos_hideslices.png
index 83449ecc3a0..72e8241f847 100644
Binary files a/test/image/baselines/pie_scale_textpos_hideslices.png and b/test/image/baselines/pie_scale_textpos_hideslices.png differ
diff --git a/test/image/baselines/pie_simple.png b/test/image/baselines/pie_simple.png
index 7297df72315..516515244a7 100644
Binary files a/test/image/baselines/pie_simple.png and b/test/image/baselines/pie_simple.png differ
diff --git a/test/image/baselines/plot_types.png b/test/image/baselines/plot_types.png
index a89b8ea6770..dfe2fe6d4f3 100644
Binary files a/test/image/baselines/plot_types.png and b/test/image/baselines/plot_types.png differ
diff --git a/test/image/baselines/pseudo_html.png b/test/image/baselines/pseudo_html.png
index f92ebd40ba5..d15e9753126 100644
Binary files a/test/image/baselines/pseudo_html.png and b/test/image/baselines/pseudo_html.png differ
diff --git a/test/image/baselines/scatter_fill_no_opacity.png b/test/image/baselines/scatter_fill_no_opacity.png
index ecfe7043e0b..4a6d73cd45a 100644
Binary files a/test/image/baselines/scatter_fill_no_opacity.png and b/test/image/baselines/scatter_fill_no_opacity.png differ
diff --git a/test/image/baselines/shared_axes_subplots.png b/test/image/baselines/shared_axes_subplots.png
index ee80a69385a..3a4e3dddf6f 100644
Binary files a/test/image/baselines/shared_axes_subplots.png and b/test/image/baselines/shared_axes_subplots.png differ
diff --git a/test/image/baselines/show_legend.png b/test/image/baselines/show_legend.png
index dfa1c7211e1..aa797d6593c 100644
Binary files a/test/image/baselines/show_legend.png and b/test/image/baselines/show_legend.png differ
diff --git a/test/image/baselines/simple_inset.png b/test/image/baselines/simple_inset.png
index 57bb753c15f..3d0619e7d0c 100644
Binary files a/test/image/baselines/simple_inset.png and b/test/image/baselines/simple_inset.png differ
diff --git a/test/image/baselines/simple_subplot.png b/test/image/baselines/simple_subplot.png
index 8332d85e488..f4157c2fc3b 100644
Binary files a/test/image/baselines/simple_subplot.png and b/test/image/baselines/simple_subplot.png differ
diff --git a/test/image/baselines/stacked_bar.png b/test/image/baselines/stacked_bar.png
index 5d7cc280df2..6f590eac105 100644
Binary files a/test/image/baselines/stacked_bar.png and b/test/image/baselines/stacked_bar.png differ
diff --git a/test/image/baselines/stacked_coupled_subplots.png b/test/image/baselines/stacked_coupled_subplots.png
index 78b5d501621..6324f9bafb5 100644
Binary files a/test/image/baselines/stacked_coupled_subplots.png and b/test/image/baselines/stacked_coupled_subplots.png differ
diff --git a/test/image/baselines/stacked_subplots.png b/test/image/baselines/stacked_subplots.png
index 55663802628..d16f07be42a 100644
Binary files a/test/image/baselines/stacked_subplots.png and b/test/image/baselines/stacked_subplots.png differ
diff --git a/test/image/baselines/styling_names.png b/test/image/baselines/styling_names.png
index 19ff9c424f7..010c1410b1b 100644
Binary files a/test/image/baselines/styling_names.png and b/test/image/baselines/styling_names.png differ
diff --git a/test/image/baselines/text_chart_arrays.png b/test/image/baselines/text_chart_arrays.png
index 9c5e580cdbf..26ce503ed07 100644
Binary files a/test/image/baselines/text_chart_arrays.png and b/test/image/baselines/text_chart_arrays.png differ
diff --git a/test/image/baselines/text_chart_invalid-arrays.png b/test/image/baselines/text_chart_invalid-arrays.png
index 2e2d2d571d2..d08e4a1e63c 100644
Binary files a/test/image/baselines/text_chart_invalid-arrays.png and b/test/image/baselines/text_chart_invalid-arrays.png differ
diff --git a/test/image/compare_pixels_test.js b/test/image/compare_pixels_test.js
index 260ddf54520..f160070464f 100644
--- a/test/image/compare_pixels_test.js
+++ b/test/image/compare_pixels_test.js
@@ -98,9 +98,25 @@ function testMock(fileName, t) {
         var options = {
             file: diffPath,
             highlightColor: 'purple',
-            tolerance: 0.0
+            tolerance: 1e-6
         };
 
+        /*
+         * N.B. The non-zero tolerance was added in
+         * https://github.com/plotly/plotly.js/pull/243
+         * where some legend mocks started generating different png outputs
+         * on `npm run test-image` and `npm run test-image -- mock.json`.
+         *
+         * Note that the svg outputs for the problematic mocks were the same
+         * and playing around with the batch size and timeout durations
+         * did not seem to affect the results.
+         *
+         * With the above tolerance individual `npm run test-image` and
+         * `npm run test-image -- mock.json` give the same result.
+         *
+         * Further investigation is needed.
+         */
+
         gm.compare(
             savedImagePath,
             path.join(constants.pathToTestImageBaselines, imageFileName),
diff --git a/test/image/mocks/legend_scroll.json b/test/image/mocks/legend_scroll.json
new file mode 100644
index 00000000000..0c2f4674d20
--- /dev/null
+++ b/test/image/mocks/legend_scroll.json
@@ -0,0 +1,126 @@
+{
+  "data": [
+    {
+      "x": [1,2,3],
+      "y": [1,2,3],
+      "type": "scatter"
+    },
+    {
+      "x": [1,2,3],
+      "y": [2,3,4],
+      "type": "bar"
+    },
+    {
+      "x": [1,2,3],
+      "y": [3,4,5],
+      "type": "scatter"
+    },
+    {
+      "x": [1,2,3],
+      "y": [4,5,6],
+      "type": "bar"
+    },
+    {
+      "x": [1,2,3],
+      "y": [5,6,7],
+      "type": "scatter"
+    },
+    {
+      "x": [1,2,3],
+      "y": [6,7,8],
+      "type": "bar"
+    },
+    {
+      "x": [1,2,3],
+      "y": [7,8,9],
+      "type": "scatter"
+    },
+    {
+      "x": [1,2,3],
+      "y": [8,9,10],
+      "type": "bar"
+    },
+    {
+      "x": [1,2,3],
+      "y": [9,10,11],
+      "type": "scatter"
+    },
+    {
+      "x": [1,2,3],
+      "y": [10,11,12],
+      "type": "bar"
+    },
+    {
+      "x": [1,2,3],
+      "y": [11,12,13],
+      "type": "scatter"
+    },
+    {
+      "x": [1,2,3],
+      "y": [12,13,14],
+      "type": "bar"
+    },
+    {
+      "x": [1,2,3],
+      "y": [13,14,15],
+      "type": "scatter"
+    },
+    {
+      "x": [1,2,3],
+      "y": [14,15,16],
+      "type": "bar"
+    },
+    {
+      "x": [1,2,3],
+      "y": [15,16,17],
+      "type": "scatter"
+    },
+    {
+      "x": [1,2,3],
+      "y": [16,17,18],
+      "type": "bar"
+    },
+    {
+      "x": [1,2,3],
+      "y": [17,18,19],
+      "type": "scatter"
+    },
+    {
+      "x": [1,2,3],
+      "y": [18,19,20],
+      "type": "bar"
+    },
+    {
+      "x": [1,2,3],
+      "y": [19,20,21],
+      "type": "scatter"
+    },
+    {
+      "x": [1,2,3],
+      "y": [20,21,22],
+      "type": "bar"
+    },
+    {
+      "x": [1,2,3],
+      "y": [21,22,23],
+      "type": "scatter"
+    },
+    {
+      "x": [1,2,3],
+      "y": [22,23,24],
+      "type": "bar"
+    },
+    {
+      "x": [1,2,3],
+      "y": [23,24,25],
+      "type": "scatter"
+    }
+  ],
+  "layout": {
+    "legend": {
+      "bordercolor": "#000000",
+      "borderwidth": 1,
+      "bgcolor": "#eeffee"
+    }
+  }
+}
diff --git a/test/jasmine/tests/legend_scroll_test.js b/test/jasmine/tests/legend_scroll_test.js
new file mode 100644
index 00000000000..e8f02a1661e
--- /dev/null
+++ b/test/jasmine/tests/legend_scroll_test.js
@@ -0,0 +1,89 @@
+var Plotly = require('@lib/index');
+var createGraph = require('../assets/create_graph_div');
+var destroyGraph = require('../assets/destroy_graph_div');
+var mock = require('../../image/mocks/legend_scroll.json');
+
+describe('The legend', function() {
+    var gd,
+        legend;
+
+    describe('when plotted with many traces', function() {
+        beforeEach(function() {
+            gd = createGraph();
+            Plotly.plot(gd, mock.data, mock.layout);
+            legend = document.getElementsByClassName('legend')[0];
+        });
+
+        afterEach(destroyGraph);
+
+        it('should not exceed plot height', function() {
+            var legendHeight = legend.getAttribute('height'),
+                plotHeight = gd._fullLayout.height - gd._fullLayout.margin.t - gd._fullLayout.margin.b;
+
+            expect(+legendHeight).toBe(plotHeight);
+        });
+
+        it('should insert a scrollbar', function() {
+            var scrollBar = legend.getElementsByClassName('scrollbar')[0];
+
+            expect(scrollBar).toBeDefined();
+            expect(scrollBar.getAttribute('x')).not.toBe(null);
+        });
+
+        it('should scroll when there\'s a wheel event', function() {
+            var scrollBox = legend.getElementsByClassName('scrollbox')[0];
+
+            legend.dispatchEvent(scrollTo(100));
+
+            // Compare against -5 because of a scroll factor of 20
+            // ( 100 / 20 === 5 )
+            expect(scrollBox.getAttribute('transform')).toBe('translate(0, -5)');
+            expect(scrollBox.getAttribute('data-scroll')).toBe('-5');
+        });
+
+        it('should constrain scrolling to the contents', function() {
+            var scrollBox = legend.getElementsByClassName('scrollbox')[0];
+
+            legend.dispatchEvent(scrollTo(-100));
+            expect(scrollBox.getAttribute('transform')).toBe('translate(0, 0)');
+
+            legend.dispatchEvent(scrollTo(100000));
+            expect(scrollBox.getAttribute('transform')).toBe('translate(0, -179)');
+        });
+
+        it('should scale the scrollbar movement from top to bottom', function() {
+            var scrollBar = legend.getElementsByClassName('scrollbar')[0],
+                legendHeight = legend.getAttribute('height');
+
+            // The scrollbar is 20px tall and has 4px margins
+
+            legend.dispatchEvent(scrollTo(-1000));
+            expect(+scrollBar.getAttribute('y')).toBe(4);
+
+            legend.dispatchEvent(scrollTo(10000));
+            expect(+scrollBar.getAttribute('y')).toBe(legendHeight - 4 - 20);
+        });
+    });
+
+    describe('when plotted with few traces', function() {
+        var gd;
+
+        beforeEach(function() {
+            gd = createGraph();
+            Plotly.plot(gd, [{ x: [1,2,3], y: [2,3,4], name: 'Test' }], {});
+        });
+
+        afterEach(destroyGraph);
+
+        it('should not display the scrollbar', function() {
+            var scrollBar = document.getElementsByClassName('scrollbar')[0];
+
+            expect(scrollBar).toBeUndefined();
+        });
+    });
+});
+
+
+function scrollTo(delta) {
+    return new WheelEvent('wheel', { deltaY: delta });
+}