function _doPlot(gd, data, layout, config) {
    var frames;

    gd = Lib.getGraphDiv(gd);

    // Events.init is idempotent and bails early if gd has already been init'd
    Events.init(gd);

    if(Lib.isPlainObject(data)) {
        var obj = data;
        data = obj.data;
        layout = obj.layout;
        config = obj.config;
        frames = obj.frames;
    }

    var okToPlot = Events.triggerHandler(gd, 'plotly_beforeplot', [data, layout, config]);
    if(okToPlot === false) return Promise.reject();

    // if there's no data or layout, and this isn't yet a plotly plot
    // container, log a warning to help plotly.js users debug
    if(!data && !layout && !Lib.isPlotDiv(gd)) {
        Lib.warn('Calling _doPlot as if redrawing ' +
            'but this container doesn\'t yet have a plot.', gd);
    }

    function addFrames() {
        if(frames) {
            return exports.addFrames(gd, frames);
        }
    }

    // transfer configuration options to gd until we move over to
    // a more OO like model
    setPlotContext(gd, config);

    if(!layout) layout = {};

    // hook class for plots main container (in case of plotly.js
    // this won't be #embedded-graph or .js-tab-contents)
    d3.select(gd).classed('js-plotly-plot', true);

    // off-screen getBoundingClientRect testing space,
    // in #js-plotly-tester (and stored as Drawing.tester)
    // so we can share cached text across tabs
    Drawing.makeTester();

    // collect promises for any async actions during plotting
    // any part of the plotting code can push to gd._promises, then
    // before we move to the next step, we check that they're all
    // complete, and empty out the promise list again.
    if(!Array.isArray(gd._promises)) gd._promises = [];

    var graphWasEmpty = ((gd.data || []).length === 0 && Array.isArray(data));

    // if there is already data on the graph, append the new data
    // if you only want to redraw, pass a non-array for data
    if(Array.isArray(data)) {
        helpers.cleanData(data);

        if(graphWasEmpty) gd.data = data;
        else gd.data.push.apply(gd.data, data);

        // for routines outside graph_obj that want a clean tab
        // (rather than appending to an existing one) gd.empty
        // is used to determine whether to make a new tab
        gd.empty = false;
    }

    if(!gd.layout || graphWasEmpty) {
        gd.layout = helpers.cleanLayout(layout);
    }

    Plots.supplyDefaults(gd);

    var fullLayout = gd._fullLayout;
    var hasCartesian = fullLayout._has('cartesian');

    // so we don't try to re-call _doPlot from inside
    // legend and colorbar, if margins changed
    fullLayout._replotting = true;

    // make or remake the framework if we need to
    if(graphWasEmpty || fullLayout._shouldCreateBgLayer) {
        makePlotFramework(gd);

        if(fullLayout._shouldCreateBgLayer) {
            delete fullLayout._shouldCreateBgLayer;
        }
    }

    // clear gradient and pattern defs on each .plot call, because we know we'll loop through all traces
    Drawing.initGradients(gd);
    Drawing.initPatterns(gd);

    // save initial show spikes once per graph
    if(graphWasEmpty) Axes.saveShowSpikeInitial(gd);

    // prepare the data and find the autorange

    // generate calcdata, if we need to
    // to force redoing calcdata, just delete it before calling _doPlot
    var recalc = !gd.calcdata || gd.calcdata.length !== (gd._fullData || []).length;
    if(recalc) Plots.doCalcdata(gd);

    // in case it has changed, attach fullData traces to calcdata
    for(var i = 0; i < gd.calcdata.length; i++) {
        gd.calcdata[i][0].trace = gd._fullData[i];
    }

    // make the figure responsive
    if(gd._context.responsive) {
        if(!gd._responsiveChartHandler) {
            // Keep a reference to the resize handler to purge it down the road
            gd._responsiveChartHandler = function() { if(!Lib.isHidden(gd)) Plots.resize(gd); };

            // Listen to window resize
            window.addEventListener('resize', gd._responsiveChartHandler);
        }
    } else {
        Lib.clearResponsive(gd);
    }

    /*
     * start async-friendly code - now we're actually drawing things
     */

    var oldMargins = Lib.extendFlat({}, fullLayout._size);

    // draw framework first so that margin-pushing
    // components can position themselves correctly
    var drawFrameworkCalls = 0;
    function drawFramework() {
        var basePlotModules = fullLayout._basePlotModules;

        for(var i = 0; i < basePlotModules.length; i++) {
            if(basePlotModules[i].drawFramework) {
                basePlotModules[i].drawFramework(gd);
            }
        }

        if(!fullLayout._glcanvas && fullLayout._has('gl')) {
            fullLayout._glcanvas = fullLayout._glcontainer.selectAll('.gl-canvas').data([{
                key: 'contextLayer',
                context: true,
                pick: false
            }, {
                key: 'focusLayer',
                context: false,
                pick: false
            }, {
                key: 'pickLayer',
                context: false,
                pick: true
            }], function(d) { return d.key; });

            fullLayout._glcanvas.enter().append('canvas')
                .attr('class', function(d) {
                    return 'gl-canvas gl-canvas-' + d.key.replace('Layer', '');
                })
                .style({
                    position: 'absolute',
                    top: 0,
                    left: 0,
                    overflow: 'visible',
                    'pointer-events': 'none'
                });
        }

        var plotGlPixelRatio = gd._context.plotGlPixelRatio;
        if(fullLayout._glcanvas) {
            fullLayout._glcanvas
                .attr('width', fullLayout.width * plotGlPixelRatio)
                .attr('height', fullLayout.height * plotGlPixelRatio)
                .style('width', fullLayout.width + 'px')
                .style('height', fullLayout.height + 'px');

            var regl = fullLayout._glcanvas.data()[0].regl;
            if(regl) {
                // Unfortunately, this can happen when relayouting to large
                // width/height on some browsers.
                if(Math.floor(fullLayout.width * plotGlPixelRatio) !== regl._gl.drawingBufferWidth ||
                    Math.floor(fullLayout.height * plotGlPixelRatio) !== regl._gl.drawingBufferHeight
                 ) {
                    var msg = 'WebGL context buffer and canvas dimensions do not match due to browser/WebGL bug.';
                    if(drawFrameworkCalls) {
                        Lib.error(msg);
                    } else {
                        Lib.log(msg + ' Clearing graph and plotting again.');
                        Plots.cleanPlot([], {}, gd._fullData, fullLayout);
                        Plots.supplyDefaults(gd);
                        fullLayout = gd._fullLayout;
                        Plots.doCalcdata(gd);
                        drawFrameworkCalls++;
                        return drawFramework();
                    }
                }
            }
        }

        if(fullLayout.modebar.orientation === 'h') {
            fullLayout._modebardiv
              .style('height', null)
              .style('width', '100%');
        } else {
            fullLayout._modebardiv
              .style('width', null)
              .style('height', fullLayout.height + 'px');
        }

        return Plots.previousPromises(gd);
    }

    // draw anything that can affect margins.
    function marginPushers() {
        // First reset the list of things that are allowed to change the margins
        // So any deleted traces or components will be wiped out of the
        // automargin calculation.
        // This means *every* margin pusher must be listed here, even if it
        // doesn't actually try to push the margins until later.
        Plots.clearAutoMarginIds(gd);

        subroutines.drawMarginPushers(gd);
        Axes.allowAutoMargin(gd);
        if(gd._fullLayout.title.text && gd._fullLayout.title.automargin) Plots.allowAutoMargin(gd, 'title.automargin');

        // TODO can this be moved elsewhere?
        if(fullLayout._has('pie')) {
            var fullData = gd._fullData;
            for(var i = 0; i < fullData.length; i++) {
                var trace = fullData[i];
                if(trace.type === 'pie' && trace.automargin) {
                    Plots.allowAutoMargin(gd, 'pie.' + trace.uid + '.automargin');
                }
            }
        }

        Plots.doAutoMargin(gd);
        return Plots.previousPromises(gd);
    }

    // in case the margins changed, draw margin pushers again
    function marginPushersAgain() {
        if(!Plots.didMarginChange(oldMargins, fullLayout._size)) return;

        return Lib.syncOrAsync([
            marginPushers,
            subroutines.layoutStyles
        ], gd);
    }

    function positionAndAutorange() {
        if(!recalc) {
            doAutoRangeAndConstraints();
            return;
        }

        // TODO: autosize extra for text markers and images
        // see https://github.com/plotly/plotly.js/issues/1111
        return Lib.syncOrAsync([
            Registry.getComponentMethod('shapes', 'calcAutorange'),
            Registry.getComponentMethod('annotations', 'calcAutorange'),
            doAutoRangeAndConstraints
        ], gd);
    }

    function doAutoRangeAndConstraints() {
        if(gd._transitioning) return;

        subroutines.doAutoRangeAndConstraints(gd);

        // store initial ranges *after* enforcing constraints, otherwise
        // we will never look like we're at the initial ranges
        if(graphWasEmpty) Axes.saveRangeInitial(gd);

        // this one is different from shapes/annotations calcAutorange
        // the others incorporate those components into ax._extremes,
        // this one actually sets the ranges in rangesliders.
        Registry.getComponentMethod('rangeslider', 'calcAutorange')(gd);
    }

    // draw ticks, titles, and calculate axis scaling (._b, ._m)
    function drawAxes() {
        return Axes.draw(gd, graphWasEmpty ? '' : 'redraw');
    }

    var seq = [
        Plots.previousPromises,
        addFrames,
        drawFramework,
        marginPushers,
        marginPushersAgain
    ];

    if(hasCartesian) seq.push(positionAndAutorange);

    seq.push(subroutines.layoutStyles);
    if(hasCartesian) {
        seq.push(
            drawAxes,
            function insideTickLabelsAutorange(gd) {
                var insideTickLabelsUpdaterange = gd._fullLayout._insideTickLabelsUpdaterange;
                if(insideTickLabelsUpdaterange) {
                    gd._fullLayout._insideTickLabelsUpdaterange = undefined;

                    return relayout(gd, insideTickLabelsUpdaterange).then(function() {
                        Axes.saveRangeInitial(gd, true);
                    });
                }
            }
        );
    }

    seq.push(
        subroutines.drawData,
        subroutines.finalDraw,
        initInteractions,
        Plots.addLinks,
        Plots.rehover,
        Plots.redrag,
        Plots.reselect,
        // TODO: doAutoMargin is only needed here for axis automargin, which
        // happens outside of marginPushers where all the other automargins are
        // calculated. Would be much better to separate margin calculations from
        // component drawing - see https://github.com/plotly/plotly.js/issues/2704
        Plots.doAutoMargin,
        Plots.previousPromises
    );

    // even if everything we did was synchronous, return a promise
    // so that the caller doesn't care which route we took
    var plotDone = Lib.syncOrAsync(seq, gd);
    if(!plotDone || !plotDone.then) plotDone = Promise.resolve();

    return plotDone.then(function() {
        emitAfterPlot(gd);
        return gd;
    });
}
Internal plot-creation function

@param {string id or DOM element} gd
     the id or DOM element of the graph container div
@param {array of objects} data
     array of traces, containing the data and display information for each trace
@param {object} layout
     object describing the overall display of the plot,
     all the stuff that doesn't pertain to any individual trace
@param {object} config
     configuration options (see ./plot_config.js for more info)

OR

@param {string id or DOM element} gd
     the id or DOM element of the graph container div
@param {object} figure
     object containing `data`, `layout`, `config`, and `frames` members