/**
* Copyright 2012-2016, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/


'use strict';


var d3 = require('d3');
var isNumeric = require('fast-isnumeric');

var Plotly = require('../plotly');
var Lib = require('../lib');
var Events = require('../lib/events');
var Queue = require('../lib/queue');

var Registry = require('../registry');
var Plots = require('../plots/plots');
var Fx = require('../plots/cartesian/graph_interact');
var Polar = require('../plots/polar');

var Drawing = require('../components/drawing');
var ErrorBars = require('../components/errorbars');
var xmlnsNamespaces = require('../constants/xmlns_namespaces');
var svgTextUtils = require('../lib/svg_text_utils');

var helpers = require('./helpers');
var subroutines = require('./subroutines');


/**
 * Main 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)
 *
 */
Plotly.plot = function(gd, data, layout, config) {
    gd = helpers.getGraphDiv(gd);

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

    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 Plotly.plot as if redrawing ' +
            'but this container doesn\'t yet have a plot.', gd);
    }

    // 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 gd._tester)
    // so we can share cached text across tabs
    Drawing.makeTester(gd);

    // 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.
    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, gd.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);

    // if the user is trying to drag the axes, allow new data and layout
    // to come in but don't allow a replot.
    if(gd._dragging && !gd._transitioning) {
        // signal to drag handler that after everything else is done
        // we need to replot, because something has changed
        gd._replotPending = true;
        return Promise.reject();
    } else {
        // we're going ahead with a replot now
        gd._replotPending = false;
    }

    Plots.supplyDefaults(gd);

    // Polar plots
    if(data && data[0] && data[0].r) return plotPolar(gd, data, layout);

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

    // make or remake the framework if we need to
    if(graphWasEmpty) makePlotFramework(gd);

    // polar need a different framework
    if(gd.framework !== makePlotFramework) {
        gd.framework = makePlotFramework;
        makePlotFramework(gd);
    }

    // save initial axis range once per graph
    if(graphWasEmpty) Plotly.Axes.saveRangeInitial(gd);

    var fullLayout = gd._fullLayout;

    // prepare the data and find the autorange

    // generate calcdata, if we need to
    // to force redoing calcdata, just delete it before calling Plotly.plot
    var recalc = !gd.calcdata || gd.calcdata.length !== (gd.data || []).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];
    }

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

    var oldmargins = JSON.stringify(fullLayout._size);

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

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

        return Lib.syncOrAsync([
            subroutines.layoutStyles,
            drawAxes,
            Fx.init
        ], gd);
    }

    // draw anything that can affect margins.
    // currently this is legend and colorbars
    function marginPushers() {
        var calcdata = gd.calcdata;
        var i, cd, trace;

        Registry.getComponentMethod('legend', 'draw')(gd);
        Registry.getComponentMethod('rangeselector', 'draw')(gd);
        Registry.getComponentMethod('updatemenus', 'draw')(gd);

        for(i = 0; i < calcdata.length; i++) {
            cd = calcdata[i];
            trace = cd[0].trace;
            if(trace.visible !== true || !trace._module.colorbar) {
                Plots.autoMargin(gd, 'cb' + trace.uid);
            }
            else trace._module.colorbar(gd, cd);
        }

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

    function marginPushersAgain() {
        // in case the margins changed, draw margin pushers again
        var seq = JSON.stringify(fullLayout._size) === oldmargins ?
            [] :
            [marginPushers, subroutines.layoutStyles];

        return Lib.syncOrAsync(seq, gd);
    }

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

        var subplots = Plots.getSubplotIds(fullLayout, 'cartesian'),
            modules = fullLayout._modules;

        // position and range calculations for traces that
        // depend on each other ie bars (stacked or grouped)
        // and boxes (grouped) push each other out of the way

        var subplotInfo, _module;

        for(var i = 0; i < subplots.length; i++) {
            subplotInfo = fullLayout._plots[subplots[i]];

            for(var j = 0; j < modules.length; j++) {
                _module = modules[j];
                if(_module.setPositions) _module.setPositions(gd, subplotInfo);
            }
        }

        // calc and autorange for errorbars
        ErrorBars.calc(gd);

        // TODO: autosize extra for text markers
        return Lib.syncOrAsync([
            Registry.getComponentMethod('shapes', 'calcAutorange'),
            Registry.getComponentMethod('annotations', 'calcAutorange'),
            doAutoRange
        ], gd);
    }

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

        var axList = Plotly.Axes.list(gd, '', true);
        for(var i = 0; i < axList.length; i++) {
            Plotly.Axes.doAutoRange(axList[i]);
        }
    }

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

    // Now plot the data
    function drawData() {
        var calcdata = gd.calcdata,
            i;

        // in case of traces that were heatmaps or contour maps
        // previously, remove them and their colorbars explicitly
        for(i = 0; i < calcdata.length; i++) {
            var trace = calcdata[i][0].trace,
                isVisible = (trace.visible === true),
                uid = trace.uid;

            if(!isVisible || !Registry.traceIs(trace, '2dMap')) {
                fullLayout._paper.selectAll(
                    '.hm' + uid +
                    ',.contour' + uid +
                    ',#clip' + uid
                ).remove();
            }

            if(!isVisible || !trace._module.colorbar) {
                fullLayout._infolayer.selectAll('.cb' + uid).remove();
            }
        }

        // loop over the base plot modules present on graph
        var basePlotModules = fullLayout._basePlotModules;
        for(i = 0; i < basePlotModules.length; i++) {
            basePlotModules[i].plot(gd);
        }

        // keep reference to shape layers in subplots
        var layerSubplot = fullLayout._paper.selectAll('.layer-subplot');
        fullLayout._imageSubplotLayer = layerSubplot.selectAll('.imagelayer');
        fullLayout._shapeSubplotLayer = layerSubplot.selectAll('.shapelayer');

        // styling separate from drawing
        Plots.style(gd);

        // show annotations and shapes
        Registry.getComponentMethod('shapes', 'draw')(gd);
        Registry.getComponentMethod('annoations', 'draw')(gd);

        // 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() {
        Registry.getComponentMethod('shapes', 'draw')(gd);
        Registry.getComponentMethod('images', 'draw')(gd);
        Registry.getComponentMethod('annotations', 'draw')(gd);
        Registry.getComponentMethod('legend', 'draw')(gd);
        Registry.getComponentMethod('rangeslider', 'draw')(gd);
        Registry.getComponentMethod('rangeselector', 'draw')(gd);
        Registry.getComponentMethod('updatemenus', 'draw')(gd);
    }

    function cleanUp() {
        // now we're REALLY TRULY done plotting...
        // so mark it as done and let other procedures call a replot
        gd.emit('plotly_afterplot');
    }

    Lib.syncOrAsync([
        Plots.previousPromises,
        drawFramework,
        marginPushers,
        marginPushersAgain,
        positionAndAutorange,
        subroutines.layoutStyles,
        drawAxes,
        drawData,
        finalDraw
    ], gd, cleanUp);

    // even if everything we did was synchronous, return a promise
    // so that the caller doesn't care which route we took
    return Promise.all(gd._promises).then(function() {
        return gd;
    });
};


function opaqueSetBackground(gd, bgColor) {
    gd._fullLayout._paperdiv.style('background', 'white');
    Plotly.defaultConfig.setBackground(gd, bgColor);
}

function setPlotContext(gd, config) {
    if(!gd._context) gd._context = Lib.extendFlat({}, Plotly.defaultConfig);
    var context = gd._context;

    if(config) {
        Object.keys(config).forEach(function(key) {
            if(key in context) {
                if(key === 'setBackground' && config[key] === 'opaque') {
                    context[key] = opaqueSetBackground;
                }
                else context[key] = config[key];
            }
        });

        // map plot3dPixelRatio to plotGlPixelRatio for backward compatibility
        if(config.plot3dPixelRatio && !context.plotGlPixelRatio) {
            context.plotGlPixelRatio = context.plot3dPixelRatio;
        }
    }

    // staticPlot forces a bunch of others:
    if(context.staticPlot) {
        context.editable = false;
        context.autosizable = false;
        context.scrollZoom = false;
        context.doubleClick = false;
        context.showTips = false;
        context.showLink = false;
        context.displayModeBar = false;
    }
}

function plotPolar(gd, data, layout) {
    // build or reuse the container skeleton
    var plotContainer = d3.select(gd).selectAll('.plot-container')
        .data([0]);
    plotContainer.enter()
        .insert('div', ':first-child')
        .classed('plot-container plotly', true);
    var paperDiv = plotContainer.selectAll('.svg-container')
        .data([0]);
    paperDiv.enter().append('div')
        .classed('svg-container', true)
        .style('position', 'relative');

    // empty it everytime for now
    paperDiv.html('');

    // fulfill gd requirements
    if(data) gd.data = data;
    if(layout) gd.layout = layout;
    Polar.manager.fillLayout(gd);

    if(gd._fullLayout.autosize === 'initial' && gd._context.autosizable) {
        plotAutoSize(gd, {});
        gd._fullLayout.autosize = layout.autosize = true;
    }
    // resize canvas
    paperDiv.style({
        width: gd._fullLayout.width + 'px',
        height: gd._fullLayout.height + 'px'
    });

    // instantiate framework
    gd.framework = Polar.manager.framework(gd);

    // plot
    gd.framework({data: gd.data, layout: gd.layout}, paperDiv.node());

    // set undo point
    gd.framework.setUndoPoint();

    // get the resulting svg for extending it
    var polarPlotSVG = gd.framework.svg();

    // editable title
    var opacity = 1;
    var txt = gd._fullLayout.title;
    if(txt === '' || !txt) opacity = 0;
    var placeholderText = 'Click to enter title';

    var titleLayout = function() {
        this.call(svgTextUtils.convertToTspans);
        // TODO: html/mathjax
        // TODO: center title
    };

    var title = polarPlotSVG.select('.title-group text')
        .call(titleLayout);

    if(gd._context.editable) {
        title.attr({'data-unformatted': txt});
        if(!txt || txt === placeholderText) {
            opacity = 0.2;
            title.attr({'data-unformatted': placeholderText})
                .text(placeholderText)
                .style({opacity: opacity})
                .on('mouseover.opacity', function() {
                    d3.select(this).transition().duration(100)
                        .style('opacity', 1);
                })
                .on('mouseout.opacity', function() {
                    d3.select(this).transition().duration(1000)
                        .style('opacity', 0);
                });
        }

        var setContenteditable = function() {
            this.call(svgTextUtils.makeEditable)
                .on('edit', function(text) {
                    gd.framework({layout: {title: text}});
                    this.attr({'data-unformatted': text})
                        .text(text)
                        .call(titleLayout);
                    this.call(setContenteditable);
                })
                .on('cancel', function() {
                    var txt = this.attr('data-unformatted');
                    this.text(txt).call(titleLayout);
                });
        };
        title.call(setContenteditable);
    }

    gd._context.setBackground(gd, gd._fullLayout.paper_bgcolor);
    Plots.addLinks(gd);

    return Promise.resolve();
}

// convenience function to force a full redraw, mostly for use by plotly.js
Plotly.redraw = function(gd) {
    gd = helpers.getGraphDiv(gd);

    if(!Lib.isPlotDiv(gd)) {
        throw new Error('This element is not a Plotly plot: ' + gd);
    }

    gd.calcdata = undefined;
    return Plotly.plot(gd).then(function() {
        gd.emit('plotly_redraw');
        return gd;
    });
};

/**
 * Convenience function to make idempotent plot option obvious to users.
 *
 * @param gd
 * @param {Object[]} data
 * @param {Object} layout
 * @param {Object} config
 */
Plotly.newPlot = function(gd, data, layout, config) {
    gd = helpers.getGraphDiv(gd);

    // remove gl contexts
    Plots.cleanPlot([], {}, gd._fullData || {}, gd._fullLayout || {});

    Plots.purge(gd);
    return Plotly.plot(gd, data, layout, config);
};

/**
 * Wrap negative indicies to their positive counterparts.
 *
 * @param {Number[]} indices An array of indices
 * @param {Number} maxIndex The maximum index allowable (arr.length - 1)
 */
function positivifyIndices(indices, maxIndex) {
    var parentLength = maxIndex + 1,
        positiveIndices = [],
        i,
        index;

    for(i = 0; i < indices.length; i++) {
        index = indices[i];
        if(index < 0) {
            positiveIndices.push(parentLength + index);
        } else {
            positiveIndices.push(index);
        }
    }
    return positiveIndices;
}

/**
 * Ensures that an index array for manipulating gd.data is valid.
 *
 * Intended for use with addTraces, deleteTraces, and moveTraces.
 *
 * @param gd
 * @param indices
 * @param arrayName
 */
function assertIndexArray(gd, indices, arrayName) {
    var i,
        index;

    for(i = 0; i < indices.length; i++) {
        index = indices[i];

        // validate that indices are indeed integers
        if(index !== parseInt(index, 10)) {
            throw new Error('all values in ' + arrayName + ' must be integers');
        }

        // check that all indices are in bounds for given gd.data array length
        if(index >= gd.data.length || index < -gd.data.length) {
            throw new Error(arrayName + ' must be valid indices for gd.data.');
        }

        // check that indices aren't repeated
        if(indices.indexOf(index, i + 1) > -1 ||
                index >= 0 && indices.indexOf(-gd.data.length + index) > -1 ||
                index < 0 && indices.indexOf(gd.data.length + index) > -1) {
            throw new Error('each index in ' + arrayName + ' must be unique.');
        }
    }
}

/**
 * Private function used by Plotly.moveTraces to check input args
 *
 * @param gd
 * @param currentIndices
 * @param newIndices
 */
function checkMoveTracesArgs(gd, currentIndices, newIndices) {

    // check that gd has attribute 'data' and 'data' is array
    if(!Array.isArray(gd.data)) {
        throw new Error('gd.data must be an array.');
    }

    // validate currentIndices array
    if(typeof currentIndices === 'undefined') {
        throw new Error('currentIndices is a required argument.');
    } else if(!Array.isArray(currentIndices)) {
        currentIndices = [currentIndices];
    }
    assertIndexArray(gd, currentIndices, 'currentIndices');

    // validate newIndices array if it exists
    if(typeof newIndices !== 'undefined' && !Array.isArray(newIndices)) {
        newIndices = [newIndices];
    }
    if(typeof newIndices !== 'undefined') {
        assertIndexArray(gd, newIndices, 'newIndices');
    }

    // check currentIndices and newIndices are the same length if newIdices exists
    if(typeof newIndices !== 'undefined' && currentIndices.length !== newIndices.length) {
        throw new Error('current and new indices must be of equal length.');
    }

}
/**
 * A private function to reduce the type checking clutter in addTraces.
 *
 * @param gd
 * @param traces
 * @param newIndices
 */
function checkAddTracesArgs(gd, traces, newIndices) {
    var i,
        value;

    // check that gd has attribute 'data' and 'data' is array
    if(!Array.isArray(gd.data)) {
        throw new Error('gd.data must be an array.');
    }

    // make sure traces exists
    if(typeof traces === 'undefined') {
        throw new Error('traces must be defined.');
    }

    // make sure traces is an array
    if(!Array.isArray(traces)) {
        traces = [traces];
    }

    // make sure each value in traces is an object
    for(i = 0; i < traces.length; i++) {
        value = traces[i];
        if(typeof value !== 'object' || (Array.isArray(value) || value === null)) {
            throw new Error('all values in traces array must be non-array objects');
        }
    }

    // make sure we have an index for each trace
    if(typeof newIndices !== 'undefined' && !Array.isArray(newIndices)) {
        newIndices = [newIndices];
    }
    if(typeof newIndices !== 'undefined' && newIndices.length !== traces.length) {
        throw new Error(
            'if indices is specified, traces.length must equal indices.length'
        );
    }
}

/**
 * A private function to reduce the type checking clutter in spliceTraces.
 * Get all update Properties from gd.data. Validate inputs and outputs.
 * Used by prependTrace and extendTraces
 *
 * @param gd
 * @param update
 * @param indices
 * @param maxPoints
 */
function assertExtendTracesArgs(gd, update, indices, maxPoints) {

    var maxPointsIsObject = Lib.isPlainObject(maxPoints);

    if(!Array.isArray(gd.data)) {
        throw new Error('gd.data must be an array');
    }
    if(!Lib.isPlainObject(update)) {
        throw new Error('update must be a key:value object');
    }

    if(typeof indices === 'undefined') {
        throw new Error('indices must be an integer or array of integers');
    }

    assertIndexArray(gd, indices, 'indices');

    for(var key in update) {

        /*
         * Verify that the attribute to be updated contains as many trace updates
         * as indices. Failure must result in throw and no-op
         */
        if(!Array.isArray(update[key]) || update[key].length !== indices.length) {
            throw new Error('attribute ' + key + ' must be an array of length equal to indices array length');
        }

        /*
         * if maxPoints is an object it must match keys and array lengths of 'update' 1:1
         */
        if(maxPointsIsObject &&
            (!(key in maxPoints) || !Array.isArray(maxPoints[key]) ||
            maxPoints[key].length !== update[key].length)) {
            throw new Error('when maxPoints is set as a key:value object it must contain a 1:1 ' +
                            'corrispondence with the keys and number of traces in the update object');
        }
    }
}

/**
 * A private function to reduce the type checking clutter in spliceTraces.
 *
 * @param {Object|HTMLDivElement} gd
 * @param {Object} update
 * @param {Number[]} indices
 * @param {Number||Object} maxPoints
 * @return {Object[]}
 */
function getExtendProperties(gd, update, indices, maxPoints) {

    var maxPointsIsObject = Lib.isPlainObject(maxPoints),
        updateProps = [];
    var trace, target, prop, insert, maxp;

    // allow scalar index to represent a single trace position
    if(!Array.isArray(indices)) indices = [indices];

    // negative indices are wrapped around to their positive value. Equivalent to python indexing.
    indices = positivifyIndices(indices, gd.data.length - 1);

    // loop through all update keys and traces and harvest validated data.
    for(var key in update) {

        for(var j = 0; j < indices.length; j++) {

            /*
             * Choose the trace indexed by the indices map argument and get the prop setter-getter
             * instance that references the key and value for this particular trace.
             */
            trace = gd.data[indices[j]];
            prop = Lib.nestedProperty(trace, key);

            /*
             * Target is the existing gd.data.trace.dataArray value like "x" or "marker.size"
             * Target must exist as an Array to allow the extend operation to be performed.
             */
            target = prop.get();
            insert = update[key][j];

            if(!Array.isArray(insert)) {
                throw new Error('attribute: ' + key + ' index: ' + j + ' must be an array');
            }
            if(!Array.isArray(target)) {
                throw new Error('cannot extend missing or non-array attribute: ' + key);
            }

            /*
             * maxPoints may be an object map or a scalar. If object select the key:value, else
             * Use the scalar maxPoints for all key and trace combinations.
             */
            maxp = maxPointsIsObject ? maxPoints[key][j] : maxPoints;

            // could have chosen null here, -1 just tells us to not take a window
            if(!isNumeric(maxp)) maxp = -1;

            /*
             * Wrap the nestedProperty in an object containing required data
             * for lengthening and windowing this particular trace - key combination.
             * Flooring maxp mirrors the behaviour of floats in the Array.slice JSnative function.
             */
            updateProps.push({
                prop: prop,
                target: target,
                insert: insert,
                maxp: Math.floor(maxp)
            });
        }
    }

    // all target and insertion data now validated
    return updateProps;
}

/**
 * A private function to key Extend and Prepend traces DRY
 *
 * @param {Object|HTMLDivElement} gd
 * @param {Object} update
 * @param {Number[]} indices
 * @param {Number||Object} maxPoints
 * @param {Function} lengthenArray
 * @param {Function} spliceArray
 * @return {Object}
 */
function spliceTraces(gd, update, indices, maxPoints, lengthenArray, spliceArray) {

    assertExtendTracesArgs(gd, update, indices, maxPoints);

    var updateProps = getExtendProperties(gd, update, indices, maxPoints),
        remainder = [],
        undoUpdate = {},
        undoPoints = {};
    var target, prop, maxp;

    for(var i = 0; i < updateProps.length; i++) {

        /*
         * prop is the object returned by Lib.nestedProperties
         */
        prop = updateProps[i].prop;
        maxp = updateProps[i].maxp;

        target = lengthenArray(updateProps[i].target, updateProps[i].insert);

        /*
         * If maxp is set within post-extension trace.length, splice to maxp length.
         * Otherwise skip function call as splice op will have no effect anyway.
         */
        if(maxp >= 0 && maxp < target.length) remainder = spliceArray(target, maxp);

        /*
         * to reverse this operation we need the size of the original trace as the reverse
         * operation will need to window out any lengthening operation performed in this pass.
         */
        maxp = updateProps[i].target.length;

        /*
         * Magic happens here! update gd.data.trace[key] with new array data.
         */
        prop.set(target);

        if(!Array.isArray(undoUpdate[prop.astr])) undoUpdate[prop.astr] = [];
        if(!Array.isArray(undoPoints[prop.astr])) undoPoints[prop.astr] = [];

        /*
         * build the inverse update object for the undo operation
         */
        undoUpdate[prop.astr].push(remainder);

        /*
         * build the matching maxPoints undo object containing original trace lengths.
         */
        undoPoints[prop.astr].push(maxp);
    }

    return {update: undoUpdate, maxPoints: undoPoints};
}

/**
 * extend && prepend traces at indices with update arrays, window trace lengths to maxPoints
 *
 * Extend and Prepend have identical APIs. Prepend inserts an array at the head while Extend
 * inserts an array off the tail. Prepend truncates the tail of the array - counting maxPoints
 * from the head, whereas Extend truncates the head of the array, counting backward maxPoints
 * from the tail.
 *
 * If maxPoints is undefined, nonNumeric, negative or greater than extended trace length no
 * truncation / windowing will be performed. If its zero, well the whole trace is truncated.
 *
 * @param {Object|HTMLDivElement} gd The graph div
 * @param {Object} update The key:array map of target attributes to extend
 * @param {Number|Number[]} indices The locations of traces to be extended
 * @param {Number|Object} [maxPoints] Number of points for trace window after lengthening.
 *
 */
Plotly.extendTraces = function extendTraces(gd, update, indices, maxPoints) {
    gd = helpers.getGraphDiv(gd);

    var undo = spliceTraces(gd, update, indices, maxPoints,

                           /*
                            * The Lengthen operation extends trace from end with insert
                            */
                            function(target, insert) {
                                return target.concat(insert);
                            },

                            /*
                             * Window the trace keeping maxPoints, counting back from the end
                             */
                            function(target, maxPoints) {
                                return target.splice(0, target.length - maxPoints);
                            });

    var promise = Plotly.redraw(gd);

    var undoArgs = [gd, undo.update, indices, undo.maxPoints];
    Queue.add(gd, Plotly.prependTraces, undoArgs, extendTraces, arguments);

    return promise;
};

Plotly.prependTraces = function prependTraces(gd, update, indices, maxPoints) {
    gd = helpers.getGraphDiv(gd);

    var undo = spliceTraces(gd, update, indices, maxPoints,

                           /*
                            * The Lengthen operation extends trace by appending insert to start
                            */
                            function(target, insert) {
                                return insert.concat(target);
                            },

                            /*
                             * Window the trace keeping maxPoints, counting forward from the start
                             */
                            function(target, maxPoints) {
                                return target.splice(maxPoints, target.length);
                            });

    var promise = Plotly.redraw(gd);

    var undoArgs = [gd, undo.update, indices, undo.maxPoints];
    Queue.add(gd, Plotly.extendTraces, undoArgs, prependTraces, arguments);

    return promise;
};

/**
 * Add data traces to an existing graph div.
 *
 * @param {Object|HTMLDivElement} gd The graph div
 * @param {Object[]} gd.data The array of traces we're adding to
 * @param {Object[]|Object} traces The object or array of objects to add
 * @param {Number[]|Number} [newIndices=[gd.data.length]] Locations to add traces
 *
 */
Plotly.addTraces = function addTraces(gd, traces, newIndices) {
    gd = helpers.getGraphDiv(gd);

    var currentIndices = [],
        undoFunc = Plotly.deleteTraces,
        redoFunc = addTraces,
        undoArgs = [gd, currentIndices],
        redoArgs = [gd, traces],  // no newIndices here
        i,
        promise;

    // all validation is done elsewhere to remove clutter here
    checkAddTracesArgs(gd, traces, newIndices);

    // make sure traces is an array
    if(!Array.isArray(traces)) {
        traces = [traces];
    }
    helpers.cleanData(traces, gd.data);

    // add the traces to gd.data (no redrawing yet!)
    for(i = 0; i < traces.length; i += 1) {
        gd.data.push(traces[i]);
    }

    // to continue, we need to call moveTraces which requires currentIndices
    for(i = 0; i < traces.length; i++) {
        currentIndices.push(-traces.length + i);
    }

    // if the user didn't define newIndices, they just want the traces appended
    // i.e., we can simply redraw and be done
    if(typeof newIndices === 'undefined') {
        promise = Plotly.redraw(gd);
        Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs);
        return promise;
    }

    // make sure indices is property defined
    if(!Array.isArray(newIndices)) {
        newIndices = [newIndices];
    }

    try {

        // this is redundant, but necessary to not catch later possible errors!
        checkMoveTracesArgs(gd, currentIndices, newIndices);
    }
    catch(error) {

        // something went wrong, reset gd to be safe and rethrow error
        gd.data.splice(gd.data.length - traces.length, traces.length);
        throw error;
    }

    // if we're here, the user has defined specific places to place the new traces
    // this requires some extra work that moveTraces will do
    Queue.startSequence(gd);
    Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs);
    promise = Plotly.moveTraces(gd, currentIndices, newIndices);
    Queue.stopSequence(gd);
    return promise;
};

/**
 * Delete traces at `indices` from gd.data array.
 *
 * @param {Object|HTMLDivElement} gd The graph div
 * @param {Object[]} gd.data The array of traces we're removing from
 * @param {Number|Number[]} indices The indices
 */
Plotly.deleteTraces = function deleteTraces(gd, indices) {
    gd = helpers.getGraphDiv(gd);

    var traces = [],
        undoFunc = Plotly.addTraces,
        redoFunc = deleteTraces,
        undoArgs = [gd, traces, indices],
        redoArgs = [gd, indices],
        i,
        deletedTrace;

    // make sure indices are defined
    if(typeof indices === 'undefined') {
        throw new Error('indices must be an integer or array of integers.');
    } else if(!Array.isArray(indices)) {
        indices = [indices];
    }
    assertIndexArray(gd, indices, 'indices');

    // convert negative indices to positive indices
    indices = positivifyIndices(indices, gd.data.length - 1);

    // we want descending here so that splicing later doesn't affect indexing
    indices.sort(Lib.sorterDes);
    for(i = 0; i < indices.length; i += 1) {
        deletedTrace = gd.data.splice(indices[i], 1)[0];
        traces.push(deletedTrace);
    }

    var promise = Plotly.redraw(gd);
    Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs);

    return promise;
};

/**
 * Move traces at currentIndices array to locations in newIndices array.
 *
 * If newIndices is omitted, currentIndices will be moved to the end. E.g.,
 * these are equivalent:
 *
 * Plotly.moveTraces(gd, [1, 2, 3], [-3, -2, -1])
 * Plotly.moveTraces(gd, [1, 2, 3])
 *
 * @param {Object|HTMLDivElement} gd The graph div
 * @param {Object[]} gd.data The array of traces we're removing from
 * @param {Number|Number[]} currentIndices The locations of traces to be moved
 * @param {Number|Number[]} [newIndices] The locations to move traces to
 *
 * Example calls:
 *
 *      // move trace i to location x
 *      Plotly.moveTraces(gd, i, x)
 *
 *      // move trace i to end of array
 *      Plotly.moveTraces(gd, i)
 *
 *      // move traces i, j, k to end of array (i != j != k)
 *      Plotly.moveTraces(gd, [i, j, k])
 *
 *      // move traces [i, j, k] to [x, y, z] (i != j != k) (x != y != z)
 *      Plotly.moveTraces(gd, [i, j, k], [x, y, z])
 *
 *      // reorder all traces (assume there are 5--a, b, c, d, e)
 *      Plotly.moveTraces(gd, [b, d, e, a, c])  // same as 'move to end'
 */
Plotly.moveTraces = function moveTraces(gd, currentIndices, newIndices) {
    gd = helpers.getGraphDiv(gd);

    var newData = [],
        movingTraceMap = [],
        undoFunc = moveTraces,
        redoFunc = moveTraces,
        undoArgs = [gd, newIndices, currentIndices],
        redoArgs = [gd, currentIndices, newIndices],
        i;

    // to reduce complexity here, check args elsewhere
    // this throws errors where appropriate
    checkMoveTracesArgs(gd, currentIndices, newIndices);

    // make sure currentIndices is an array
    currentIndices = Array.isArray(currentIndices) ? currentIndices : [currentIndices];

    // if undefined, define newIndices to point to the end of gd.data array
    if(typeof newIndices === 'undefined') {
        newIndices = [];
        for(i = 0; i < currentIndices.length; i++) {
            newIndices.push(-currentIndices.length + i);
        }
    }

    // make sure newIndices is an array if it's user-defined
    newIndices = Array.isArray(newIndices) ? newIndices : [newIndices];

    // convert negative indices to positive indices (they're the same length)
    currentIndices = positivifyIndices(currentIndices, gd.data.length - 1);
    newIndices = positivifyIndices(newIndices, gd.data.length - 1);

    // at this point, we've coerced the index arrays into predictable forms

    // get the traces that aren't being moved around
    for(i = 0; i < gd.data.length; i++) {

        // if index isn't in currentIndices, include it in ignored!
        if(currentIndices.indexOf(i) === -1) {
            newData.push(gd.data[i]);
        }
    }

    // get a mapping of indices to moving traces
    for(i = 0; i < currentIndices.length; i++) {
        movingTraceMap.push({newIndex: newIndices[i], trace: gd.data[currentIndices[i]]});
    }

    // reorder this mapping by newIndex, ascending
    movingTraceMap.sort(function(a, b) {
        return a.newIndex - b.newIndex;
    });

    // now, add the moving traces back in, in order!
    for(i = 0; i < movingTraceMap.length; i += 1) {
        newData.splice(movingTraceMap[i].newIndex, 0, movingTraceMap[i].trace);
    }

    gd.data = newData;

    var promise = Plotly.redraw(gd);
    Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs);

    return promise;
};

/**
 * restyle: update trace attributes of an existing plot
 *
 * Can be called two ways.
 *
 * Signature 1:
 * @param {String | HTMLDivElement} gd
 *  the id or DOM element of the graph container div
 * @param {String} astr
 *  attribute string (like `'marker.symbol'`) to update
 * @param {*} val
 *  value to give this attribute
 * @param {Number[] | Number} [traces]
 *  integer or array of integers for the traces to alter (all if omitted)
 *
 * Signature 2:
 * @param {String | HTMLDivElement} gd
 *  (as in signature 1)
 * @param {Object} aobj
 *  attribute object `{astr1: val1, astr2: val2 ...}`
 *  allows setting multiple attributes simultaneously
 * @param {Number[] | Number} [traces]
 *  (as in signature 1)
 *
 * `val` (or `val1`, `val2` ... in the object form) can be an array,
 * to apply different values to each trace.
 *
 * If the array is too short, it will wrap around (useful for
 * style files that want to specify cyclical default values).
 */
Plotly.restyle = function restyle(gd, astr, val, traces) {
    gd = helpers.getGraphDiv(gd);
    helpers.clearPromiseQueue(gd);

    var aobj = {};
    if(typeof astr === 'string') aobj[astr] = val;
    else if(Lib.isPlainObject(astr)) {
        // the 3-arg form
        aobj = astr;
        if(traces === undefined) traces = val;
    }
    else {
        Lib.warn('Restyle fail.', astr, val, traces);
        return Promise.reject();
    }

    if(Object.keys(aobj).length) gd.changed = true;

    var specs = _restyle(gd, aobj, traces),
        flags = specs.flags;

    // clear calcdata if required
    if(flags.clearCalc) gd.calcdata = undefined;

    // fill in redraw sequence
    var seq = [];

    if(flags.fullReplot) {
        seq.push(Plotly.plot);
    }
    else {
        seq.push(Plots.previousPromises);

        Plots.supplyDefaults(gd);

        if(flags.dostyle) seq.push(subroutines.doTraceStyle);
        if(flags.docolorbars) seq.push(subroutines.doColorBars);
    }

    Queue.add(gd,
        restyle, [gd, specs.undoit, specs.traces],
        restyle, [gd, specs.redoit, specs.traces]
    );

    var plotDone = Lib.syncOrAsync(seq, gd);
    if(!plotDone || !plotDone.then) plotDone = Promise.resolve();

    return plotDone.then(function() {
        gd.emit('plotly_restyle', specs.eventData);
        return gd;
    });
};

function _restyle(gd, aobj, _traces) {
    var fullLayout = gd._fullLayout,
        fullData = gd._fullData,
        data = gd.data,
        i;

    var traces = helpers.coerceTraceIndices(gd, _traces);

    // initialize flags
    var flags = {
        docalc: false,
        docalcAutorange: false,
        doplot: false,
        dostyle: false,
        docolorbars: false,
        autorangeOn: false,
        clearCalc: false,
        fullReplot: false
    };

    // copies of the change (and previous values of anything affected)
    // for the undo / redo queue
    var redoit = {},
        undoit = {},
        axlist,
        flagAxForDelete = {};

    // recalcAttrs attributes need a full regeneration of calcdata
    // as well as a replot, because the right objects may not exist,
    // or autorange may need recalculating
    // in principle we generally shouldn't need to redo ALL traces... that's
    // harder though.
    var recalcAttrs = [
        'mode', 'visible', 'type', 'orientation', 'fill',
        'histfunc', 'histnorm', 'text',
        'x', 'y', 'z',
        'a', 'b', 'c',
        'xtype', 'x0', 'dx', 'ytype', 'y0', 'dy', 'xaxis', 'yaxis',
        'line.width',
        'connectgaps', 'transpose', 'zsmooth',
        'showscale', 'marker.showscale',
        'zauto', 'marker.cauto',
        'autocolorscale', 'marker.autocolorscale',
        'colorscale', 'marker.colorscale',
        'reversescale', 'marker.reversescale',
        'autobinx', 'nbinsx', 'xbins', 'xbins.start', 'xbins.end', 'xbins.size',
        'autobiny', 'nbinsy', 'ybins', 'ybins.start', 'ybins.end', 'ybins.size',
        'autocontour', 'ncontours', 'contours', 'contours.coloring',
        'error_y', 'error_y.visible', 'error_y.value', 'error_y.type',
        'error_y.traceref', 'error_y.array', 'error_y.symmetric',
        'error_y.arrayminus', 'error_y.valueminus', 'error_y.tracerefminus',
        'error_x', 'error_x.visible', 'error_x.value', 'error_x.type',
        'error_x.traceref', 'error_x.array', 'error_x.symmetric',
        'error_x.arrayminus', 'error_x.valueminus', 'error_x.tracerefminus',
        'swapxy', 'swapxyaxes', 'orientationaxes',
        'marker.colors', 'values', 'labels', 'label0', 'dlabel', 'sort',
        'textinfo', 'textposition', 'textfont.size', 'textfont.family', 'textfont.color',
        'insidetextfont.size', 'insidetextfont.family', 'insidetextfont.color',
        'outsidetextfont.size', 'outsidetextfont.family', 'outsidetextfont.color',
        'hole', 'scalegroup', 'domain', 'domain.x', 'domain.y',
        'domain.x[0]', 'domain.x[1]', 'domain.y[0]', 'domain.y[1]',
        'tilt', 'tiltaxis', 'depth', 'direction', 'rotation', 'pull',
        'line.showscale', 'line.cauto', 'line.autocolorscale', 'line.reversescale',
        'marker.line.showscale', 'marker.line.cauto', 'marker.line.autocolorscale', 'marker.line.reversescale'
    ];

    for(i = 0; i < traces.length; i++) {
        if(Registry.traceIs(fullData[traces[i]], 'box')) {
            recalcAttrs.push('name');
            break;
        }
    }

    // autorangeAttrs attributes need a full redo of calcdata
    // only if an axis is autoranged,
    // because .calc() is where the autorange gets determined
    // TODO: could we break this out as well?
    var autorangeAttrs = [
        'marker', 'marker.size', 'textfont',
        'boxpoints', 'jitter', 'pointpos', 'whiskerwidth', 'boxmean'
    ];

    // replotAttrs attributes need a replot (because different
    // objects need to be made) but not a recalc
    var replotAttrs = [
        'zmin', 'zmax', 'zauto',
        'xgap', 'ygap',
        'marker.cmin', 'marker.cmax', 'marker.cauto',
        'line.cmin', 'line.cmax',
        'marker.line.cmin', 'marker.line.cmax',
        'contours.start', 'contours.end', 'contours.size',
        'contours.showlines',
        'line', 'line.smoothing', 'line.shape',
        'error_y.width', 'error_x.width', 'error_x.copy_ystyle',
        'marker.maxdisplayed'
    ];

    // these ones may alter the axis type
    // (at least if the first trace is involved)
    var axtypeAttrs = [
        'type', 'x', 'y', 'x0', 'y0', 'orientation', 'xaxis', 'yaxis'
    ];

    var zscl = ['zmin', 'zmax'],
        xbins = ['xbins.start', 'xbins.end', 'xbins.size'],
        ybins = ['ybins.start', 'ybins.end', 'ybins.size'],
        contourAttrs = ['contours.start', 'contours.end', 'contours.size'];

    // At the moment, only cartesian, pie and ternary plot types can afford
    // to not go through a full replot
    var doPlotWhiteList = ['cartesian', 'pie', 'ternary'];
    fullLayout._basePlotModules.forEach(function(_module) {
        if(doPlotWhiteList.indexOf(_module.name) === -1) flags.docalc = true;
    });

    // make a new empty vals array for undoit
    function a0() { return traces.map(function() { return undefined; }); }

    // for autoranging multiple axes
    function addToAxlist(axid) {
        var axName = Plotly.Axes.id2name(axid);
        if(axlist.indexOf(axName) === -1) axlist.push(axName);
    }

    function autorangeAttr(axName) { return 'LAYOUT' + axName + '.autorange'; }

    function rangeAttr(axName) { return 'LAYOUT' + axName + '.range'; }

    // for attrs that interact (like scales & autoscales), save the
    // old vals before making the change
    // val=undefined will not set a value, just record what the value was.
    // val=null will delete the attribute
    // attr can be an array to set several at once (all to the same val)
    function doextra(attr, val, i) {
        if(Array.isArray(attr)) {
            attr.forEach(function(a) { doextra(a, val, i); });
            return;
        }
        // quit if explicitly setting this elsewhere
        if(attr in aobj) return;

        var extraparam;
        if(attr.substr(0, 6) === 'LAYOUT') {
            extraparam = Lib.nestedProperty(gd.layout, attr.replace('LAYOUT', ''));
        } else {
            extraparam = Lib.nestedProperty(data[traces[i]], attr);
        }

        if(!(attr in undoit)) {
            undoit[attr] = a0();
        }
        if(undoit[attr][i] === undefined) {
            undoit[attr][i] = extraparam.get();
        }
        if(val !== undefined) {
            extraparam.set(val);
        }
    }

    // now make the changes to gd.data (and occasionally gd.layout)
    // and figure out what kind of graphics update we need to do
    for(var ai in aobj) {
        var vi = aobj[ai],
            cont,
            contFull,
            param,
            oldVal,
            newVal;

        redoit[ai] = vi;

        if(ai.substr(0, 6) === 'LAYOUT') {
            param = Lib.nestedProperty(gd.layout, ai.replace('LAYOUT', ''));
            undoit[ai] = [param.get()];
            // since we're allowing val to be an array, allow it here too,
            // even though that's meaningless
            param.set(Array.isArray(vi) ? vi[0] : vi);
            // ironically, the layout attrs in restyle only require replot,
            // not relayout
            flags.docalc = true;
            continue;
        }

        // take no chances on transforms
        if(ai.substr(0, 10) === 'transforms') flags.docalc = true;

        // set attribute in gd.data
        undoit[ai] = a0();
        for(i = 0; i < traces.length; i++) {
            cont = data[traces[i]];
            contFull = fullData[traces[i]];
            param = Lib.nestedProperty(cont, ai);
            oldVal = param.get();
            newVal = Array.isArray(vi) ? vi[i % vi.length] : vi;

            if(newVal === undefined) continue;

            // setting bin or z settings should turn off auto
            // and setting auto should save bin or z settings
            if(zscl.indexOf(ai) !== -1) {
                doextra('zauto', false, i);
            }
            else if(ai === 'colorscale') {
                doextra('autocolorscale', false, i);
            }
            else if(ai === 'autocolorscale') {
                doextra('colorscale', undefined, i);
            }
            else if(ai === 'marker.colorscale') {
                doextra('marker.autocolorscale', false, i);
            }
            else if(ai === 'marker.autocolorscale') {
                doextra('marker.colorscale', undefined, i);
            }
            else if(ai === 'zauto') {
                doextra(zscl, undefined, i);
            }
            else if(xbins.indexOf(ai) !== -1) {
                doextra('autobinx', false, i);
            }
            else if(ai === 'autobinx') {
                doextra(xbins, undefined, i);
            }
            else if(ybins.indexOf(ai) !== -1) {
                doextra('autobiny', false, i);
            }
            else if(ai === 'autobiny') {
                doextra(ybins, undefined, i);
            }
            else if(contourAttrs.indexOf(ai) !== -1) {
                doextra('autocontour', false, i);
            }
            else if(ai === 'autocontour') {
                doextra(contourAttrs, undefined, i);
            }
            // heatmaps: setting x0 or dx, y0 or dy,
            // should turn xtype/ytype to 'scaled' if 'array'
            else if(['x0', 'dx'].indexOf(ai) !== -1 &&
                    contFull.x && contFull.xtype !== 'scaled') {
                doextra('xtype', 'scaled', i);
            }
            else if(['y0', 'dy'].indexOf(ai) !== -1 &&
                    contFull.y && contFull.ytype !== 'scaled') {
                doextra('ytype', 'scaled', i);
            }
            // changing colorbar size modes,
            // make the resulting size not change
            // note that colorbar fractional sizing is based on the
            // original plot size, before anything (like a colorbar)
            // increases the margins
            else if(ai === 'colorbar.thicknessmode' && param.get() !== newVal &&
                        ['fraction', 'pixels'].indexOf(newVal) !== -1 &&
                        contFull.colorbar) {
                var thicknorm =
                    ['top', 'bottom'].indexOf(contFull.colorbar.orient) !== -1 ?
                        (fullLayout.height - fullLayout.margin.t - fullLayout.margin.b) :
                        (fullLayout.width - fullLayout.margin.l - fullLayout.margin.r);
                doextra('colorbar.thickness', contFull.colorbar.thickness *
                    (newVal === 'fraction' ? 1 / thicknorm : thicknorm), i);
            }
            else if(ai === 'colorbar.lenmode' && param.get() !== newVal &&
                        ['fraction', 'pixels'].indexOf(newVal) !== -1 &&
                        contFull.colorbar) {
                var lennorm =
                    ['top', 'bottom'].indexOf(contFull.colorbar.orient) !== -1 ?
                        (fullLayout.width - fullLayout.margin.l - fullLayout.margin.r) :
                        (fullLayout.height - fullLayout.margin.t - fullLayout.margin.b);
                doextra('colorbar.len', contFull.colorbar.len *
                    (newVal === 'fraction' ? 1 / lennorm : lennorm), i);
            }
            else if(ai === 'colorbar.tick0' || ai === 'colorbar.dtick') {
                doextra('colorbar.tickmode', 'linear', i);
            }
            else if(ai === 'colorbar.tickmode') {
                doextra(['colorbar.tick0', 'colorbar.dtick'], undefined, i);
            }


            if(ai === 'type' && (newVal === 'pie') !== (oldVal === 'pie')) {
                var labelsTo = 'x',
                    valuesTo = 'y';
                if((newVal === 'bar' || oldVal === 'bar') && cont.orientation === 'h') {
                    labelsTo = 'y';
                    valuesTo = 'x';
                }
                Lib.swapAttrs(cont, ['?', '?src'], 'labels', labelsTo);
                Lib.swapAttrs(cont, ['d?', '?0'], 'label', labelsTo);
                Lib.swapAttrs(cont, ['?', '?src'], 'values', valuesTo);

                if(oldVal === 'pie') {
                    Lib.nestedProperty(cont, 'marker.color')
                        .set(Lib.nestedProperty(cont, 'marker.colors').get());

                    // super kludgy - but if all pies are gone we won't remove them otherwise
                    fullLayout._pielayer.selectAll('g.trace').remove();
                } else if(Registry.traceIs(cont, 'cartesian')) {
                    Lib.nestedProperty(cont, 'marker.colors')
                        .set(Lib.nestedProperty(cont, 'marker.color').get());
                    // look for axes that are no longer in use and delete them
                    flagAxForDelete[cont.xaxis || 'x'] = true;
                    flagAxForDelete[cont.yaxis || 'y'] = true;
                }
            }

            undoit[ai][i] = oldVal;
            // set the new value - if val is an array, it's one el per trace
            // first check for attributes that get more complex alterations
            var swapAttrs = [
                'swapxy', 'swapxyaxes', 'orientation', 'orientationaxes'
            ];
            if(swapAttrs.indexOf(ai) !== -1) {
                // setting an orientation: make sure it's changing
                // before we swap everything else
                if(ai === 'orientation') {
                    param.set(newVal);
                    if(param.get() === undoit[ai][i]) continue;
                }
                // orientationaxes has no value,
                // it flips everything and the axes
                else if(ai === 'orientationaxes') {
                    cont.orientation =
                        {v: 'h', h: 'v'}[contFull.orientation];
                }
                helpers.swapXYData(cont);
            }
            // all the other ones, just modify that one attribute
            else param.set(newVal);

        }

        // swap the data attributes of the relevant x and y axes?
        if(['swapxyaxes', 'orientationaxes'].indexOf(ai) !== -1) {
            Plotly.Axes.swap(gd, traces);
        }

        // swap hovermode if set to "compare x/y data"
        if(ai === 'orientationaxes') {
            var hovermode = Lib.nestedProperty(gd.layout, 'hovermode');
            if(hovermode.get() === 'x') {
                hovermode.set('y');
            } else if(hovermode.get() === 'y') {
                hovermode.set('x');
            }
        }

        // check if we need to call axis type
        if((traces.indexOf(0) !== -1) && (axtypeAttrs.indexOf(ai) !== -1)) {
            Plotly.Axes.clearTypes(gd, traces);
            flags.docalc = true;
        }

        // switching from auto to manual binning or z scaling doesn't
        // actually do anything but change what you see in the styling
        // box. everything else at least needs to apply styles
        if((['autobinx', 'autobiny', 'zauto'].indexOf(ai) === -1) ||
                newVal !== false) {
            flags.dostyle = true;
        }
        if(['colorbar', 'line'].indexOf(param.parts[0]) !== -1 ||
            param.parts[0] === 'marker' && param.parts[1] === 'colorbar') {
            flags.docolorbars = true;
        }

        if(recalcAttrs.indexOf(ai) !== -1) {
            // major enough changes deserve autoscale, autobin, and
            // non-reversed axes so people don't get confused
            if(['orientation', 'type'].indexOf(ai) !== -1) {
                axlist = [];
                for(i = 0; i < traces.length; i++) {
                    var trace = data[traces[i]];

                    if(Registry.traceIs(trace, 'cartesian')) {
                        addToAxlist(trace.xaxis || 'x');
                        addToAxlist(trace.yaxis || 'y');

                        if(ai === 'type') {
                            doextra(['autobinx', 'autobiny'], true, i);
                        }
                    }
                }

                doextra(axlist.map(autorangeAttr), true, 0);
                doextra(axlist.map(rangeAttr), [0, 1], 0);
            }
            flags.docalc = true;
        }
        else if(replotAttrs.indexOf(ai) !== -1) flags.doplot = true;
        else if(autorangeAttrs.indexOf(ai) !== -1) flags.docalcAutorange = true;
    }

    // do we need to force a recalc?
    Plotly.Axes.list(gd).forEach(function(ax) {
        if(ax.autorange) flags.autorangeOn = true;
    });

    // check axes we've flagged for possible deletion
    // flagAxForDelete is a hash so we can make sure we only get each axis once
    var axListForDelete = Object.keys(flagAxForDelete);
    axisLoop:
    for(i = 0; i < axListForDelete.length; i++) {
        var axId = axListForDelete[i],
            axLetter = axId.charAt(0),
            axAttr = axLetter + 'axis';

        for(var j = 0; j < data.length; j++) {
            if(Registry.traceIs(data[j], 'cartesian') &&
                    (data[j][axAttr] || axLetter) === axId) {
                continue axisLoop;
            }
        }

        // no data on this axis - delete it.
        doextra('LAYOUT' + Plotly.Axes.id2name(axId), null, 0);
    }

    // combine a few flags together;
    if(flags.docalc || (flags.docalcAutorange && flags.autorangeOn)) {
        flags.clearCalc = true;
    }
    if(flags.docalc || flags.doplot || flags.docalcAutorange) {
        flags.fullReplot = true;
    }

    return {
        flags: flags,
        undoit: undoit,
        redoit: redoit,
        traces: traces,
        eventData: Lib.extendDeepNoArrays([], [redoit, traces])
    };
}

/**
 * relayout: update layout attributes of an existing plot
 *
 * Can be called two ways:
 *
 * Signature 1:
 * @param {String | HTMLDivElement} gd
 *  the id or dom element of the graph container div
 * @param {String} astr
 *  attribute string (like `'xaxis.range[0]'`) to update
 * @param {*} val
 *  value to give this attribute
 *
 * Signature 2:
 * @param {String | HTMLDivElement} gd
 *  (as in signature 1)
 * @param {Object} aobj
 *  attribute object `{astr1: val1, astr2: val2 ...}`
 *  allows setting multiple attributes simultaneously
 */
Plotly.relayout = function relayout(gd, astr, val) {
    gd = helpers.getGraphDiv(gd);
    helpers.clearPromiseQueue(gd);

    if(gd.framework && gd.framework.isPolar) {
        return Promise.resolve(gd);
    }

    var aobj = {};
    if(typeof astr === 'string') aobj[astr] = val;
    else if(Lib.isPlainObject(astr)) aobj = astr;
    else {
        Lib.warn('Relayout fail.', astr, val);
        return Promise.reject();
    }

    if(Object.keys(aobj).length) gd.changed = true;

    var specs = _relayout(gd, aobj),
        flags = specs.flags;

    // clear calcdata if required
    if(flags.docalc) gd.calcdata = undefined;

    // fill in redraw sequence
    var seq = [];

    if(flags.layoutReplot) {
        seq.push(subroutines.layoutReplot);
    }
    else if(Object.keys(aobj).length) {
        seq.push(Plots.previousPromises);
        Plots.supplyDefaults(gd);

        if(flags.dolegend) seq.push(subroutines.doLegend);
        if(flags.dolayoutstyle) seq.push(subroutines.layoutStyles);
        if(flags.doticks) seq.push(subroutines.doTicksRelayout);
        if(flags.domodebar) seq.push(subroutines.doModeBar);
    }

    Queue.add(gd,
        relayout, [gd, specs.undoit],
        relayout, [gd, specs.redoit]
    );

    var plotDone = Lib.syncOrAsync(seq, gd);
    if(!plotDone || !plotDone.then) plotDone = Promise.resolve(gd);

    return plotDone.then(function() {
        subroutines.setRangeSliderRange(gd, specs.eventData);
        gd.emit('plotly_relayout', specs.eventData);
        return gd;
    });
};

function _relayout(gd, aobj) {
    var layout = gd.layout,
        fullLayout = gd._fullLayout,
        keys = Object.keys(aobj),
        axes = Plotly.Axes.list(gd),
        i;

    // look for 'allaxes', split out into all axes
    // in case of 3D the axis are nested within a scene which is held in _id
    for(i = 0; i < keys.length; i++) {
        if(keys[i].indexOf('allaxes') === 0) {
            for(var j = 0; j < axes.length; j++) {
                var scene = axes[j]._id.substr(1),
                    axisAttr = (scene.indexOf('scene') !== -1) ? (scene + '.') : '',
                    newkey = keys[i].replace('allaxes', axisAttr + axes[j]._name);

                if(!aobj[newkey]) aobj[newkey] = aobj[keys[i]];
            }

            delete aobj[keys[i]];
        }
    }

    // initialize flags
    var flags = {
        dolegend: false,
        doticks: false,
        dolayoutstyle: false,
        doplot: false,
        docalc: false,
        domodebar: false,
        layoutReplot: false
    };

    // copies of the change (and previous values of anything affected)
    // for the undo / redo queue
    var redoit = {},
        undoit = {};

    var hw = ['height', 'width'];

    // for attrs that interact (like scales & autoscales), save the
    // old vals before making the change
    // val=undefined will not set a value, just record what the value was.
    // attr can be an array to set several at once (all to the same val)
    function doextra(attr, val) {
        if(Array.isArray(attr)) {
            attr.forEach(function(a) { doextra(a, val); });
            return;
        }
        // quit if explicitly setting this elsewhere
        if(attr in aobj) return;

        var p = Lib.nestedProperty(layout, attr);
        if(!(attr in undoit)) undoit[attr] = p.get();
        if(val !== undefined) p.set(val);
    }

    // for editing annotations or shapes - is it on autoscaled axes?
    function refAutorange(obj, axletter) {
        var axName = Plotly.Axes.id2name(obj[axletter + 'ref'] || axletter);
        return (fullLayout[axName] || {}).autorange;
    }

    // alter gd.layout
    for(var ai in aobj) {
        var p = Lib.nestedProperty(layout, ai),
            vi = aobj[ai],
            plen = p.parts.length,
            // p.parts may end with an index integer if the property is an array
            pend = typeof p.parts[plen - 1] === 'string' ? (plen - 1) : (plen - 2),
            // last property in chain (leaf node)
            pleaf = p.parts[pend],
            // leaf plus immediate parent
            pleafPlus = p.parts[pend - 1] + '.' + pleaf,
            // trunk nodes (everything except the leaf)
            ptrunk = p.parts.slice(0, pend).join('.'),
            parentIn = Lib.nestedProperty(gd.layout, ptrunk).get(),
            parentFull = Lib.nestedProperty(fullLayout, ptrunk).get(),
            diff;

        if(vi === undefined) continue;

        redoit[ai] = vi;

        // axis reverse is special - it is its own inverse
        // op and has no flag.
        undoit[ai] = (pleaf === 'reverse') ? vi : p.get();

        // check autosize or autorange vs size and range
        if(hw.indexOf(ai) !== -1) {
            doextra('autosize', false);
        }
        else if(ai === 'autosize') {
            doextra(hw, undefined);
        }
        else if(pleafPlus.match(/^[xyz]axis[0-9]*\.range(\[[0|1]\])?$/)) {
            doextra(ptrunk + '.autorange', false);
        }
        else if(pleafPlus.match(/^[xyz]axis[0-9]*\.autorange$/)) {
            doextra([ptrunk + '.range[0]', ptrunk + '.range[1]'],
                undefined);
        }
        else if(pleafPlus.match(/^aspectratio\.[xyz]$/)) {
            doextra(p.parts[0] + '.aspectmode', 'manual');
        }
        else if(pleafPlus.match(/^aspectmode$/)) {
            doextra([ptrunk + '.x', ptrunk + '.y', ptrunk + '.z'], undefined);
        }
        else if(pleaf === 'tick0' || pleaf === 'dtick') {
            doextra(ptrunk + '.tickmode', 'linear');
        }
        else if(pleaf === 'tickmode') {
            doextra([ptrunk + '.tick0', ptrunk + '.dtick'], undefined);
        }
        else if(/[xy]axis[0-9]*?$/.test(pleaf) && !Object.keys(vi || {}).length) {
            flags.docalc = true;
        }
        else if(/[xy]axis[0-9]*\.categoryorder$/.test(pleafPlus)) {
            flags.docalc = true;
        }
        else if(/[xy]axis[0-9]*\.categoryarray/.test(pleafPlus)) {
            flags.docalc = true;
        }

        if(pleafPlus.indexOf('rangeslider') !== -1) {
            flags.docalc = true;
        }

        // toggling log without autorange: need to also recalculate ranges
        // logical XOR (ie are we toggling log)
        if(pleaf === 'type' && ((parentFull.type === 'log') !== (vi === 'log'))) {
            var ax = parentIn;

            if(!ax || !ax.range) {
                doextra(ptrunk + '.autorange', true);
            }
            else if(!parentFull.autorange) {
                var r0 = ax.range[0],
                    r1 = ax.range[1];
                if(vi === 'log') {
                    // if both limits are negative, autorange
                    if(r0 <= 0 && r1 <= 0) {
                        doextra(ptrunk + '.autorange', true);
                    }
                    // if one is negative, set it 6 orders below the other.
                    if(r0 <= 0) r0 = r1 / 1e6;
                    else if(r1 <= 0) r1 = r0 / 1e6;
                    // now set the range values as appropriate
                    doextra(ptrunk + '.range[0]', Math.log(r0) / Math.LN10);
                    doextra(ptrunk + '.range[1]', Math.log(r1) / Math.LN10);
                }
                else {
                    doextra(ptrunk + '.range[0]', Math.pow(10, r0));
                    doextra(ptrunk + '.range[1]', Math.pow(10, r1));
                }
            }
            else if(vi === 'log') {
                // just make sure the range is positive and in the right
                // order, it'll get recalculated later
                ax.range = (ax.range[1] > ax.range[0]) ? [1, 2] : [2, 1];
            }
        }

        // handle axis reversal explicitly, as there's no 'reverse' flag
        if(pleaf === 'reverse') {
            if(parentIn.range) parentIn.range.reverse();
            else {
                doextra(ptrunk + '.autorange', true);
                parentIn.range = [1, 0];
            }

            if(parentFull.autorange) flags.docalc = true;
            else flags.doplot = true;
        }
        // send annotation and shape mods one-by-one through Annotations.draw(),
        // don't set via nestedProperty
        // that's because add and remove are special
        else if(p.parts[0] === 'annotations' || p.parts[0] === 'shapes') {
            var objNum = p.parts[1],
                objType = p.parts[0],
                objList = layout[objType] || [],
                obji = objList[objNum] || {};

            // if p.parts is just an annotation number, and val is either
            // 'add' or an entire annotation to add, the undo is 'remove'
            // if val is 'remove' then undo is the whole annotation object
            if(p.parts.length === 2) {
                if(aobj[ai] === 'add' || Lib.isPlainObject(aobj[ai])) {
                    undoit[ai] = 'remove';
                }
                else if(aobj[ai] === 'remove') {
                    if(objNum === -1) {
                        undoit[objType] = objList;
                        delete undoit[ai];
                    }
                    else undoit[ai] = obji;
                }
                else Lib.log('???', aobj);
            }

            if((refAutorange(obji, 'x') || refAutorange(obji, 'y')) &&
                    !Lib.containsAny(ai, ['color', 'opacity', 'align', 'dash'])) {
                flags.docalc = true;
            }

            // TODO: combine all edits to a given annotation / shape into one call
            // as it is we get separate calls for x and y (or ax and ay) on move

            var drawOne = Registry.getComponentMethod(objType, 'drawOne');
            drawOne(gd, objNum, p.parts.slice(2).join('.'), aobj[ai]);
            delete aobj[ai];
        }
        else if(p.parts[0] === 'images') {
            var update = Lib.objectFromPath(ai, vi);
            Lib.extendDeepAll(gd.layout, update);

            Registry.getComponentMethod('images', 'supplyLayoutDefaults')(gd.layout, gd._fullLayout);
            Registry.getComponentMethod('images', 'draw')(gd);
        }
        else if(p.parts[0] === 'mapbox' && p.parts[1] === 'layers') {
            Lib.extendDeepAll(gd.layout, Lib.objectFromPath(ai, vi));

            // append empty container to mapbox.layers
            // so that relinkPrivateKeys does not complain

            var fullLayers = (gd._fullLayout.mapbox || {}).layers || [];
            diff = (p.parts[2] + 1) - fullLayers.length;

            for(i = 0; i < diff; i++) fullLayers.push({});

            flags.doplot = true;
        }
        else if(p.parts[0] === 'updatemenus') {
            Lib.extendDeepAll(gd.layout, Lib.objectFromPath(ai, vi));

            var menus = gd._fullLayout.updatemenus || [];
            diff = (p.parts[2] + 1) - menus.length;

            for(i = 0; i < diff; i++) menus.push({});
            flags.doplot = true;
        }
        // alter gd.layout
        else {
            // check whether we can short-circuit a full redraw
            // 3d or geo at this point just needs to redraw.
            if(p.parts[0].indexOf('scene') === 0) flags.doplot = true;
            else if(p.parts[0].indexOf('geo') === 0) flags.doplot = true;
            else if(p.parts[0].indexOf('ternary') === 0) flags.doplot = true;
            else if(ai === 'paper_bgcolor') flags.doplot = true;
            else if(fullLayout._has('gl2d') &&
                (ai.indexOf('axis') !== -1 || p.parts[0] === 'plot_bgcolor')
            ) flags.doplot = true;
            else if(ai === 'hiddenlabels') flags.docalc = true;
            else if(p.parts[0].indexOf('legend') !== -1) flags.dolegend = true;
            else if(ai.indexOf('title') !== -1) flags.doticks = true;
            else if(p.parts[0].indexOf('bgcolor') !== -1) flags.dolayoutstyle = true;
            else if(p.parts.length > 1 &&
                    Lib.containsAny(p.parts[1], ['tick', 'exponent', 'grid', 'zeroline'])) {
                flags.doticks = true;
            }
            else if(ai.indexOf('.linewidth') !== -1 &&
                    ai.indexOf('axis') !== -1) {
                flags.doticks = flags.dolayoutstyle = true;
            }
            else if(p.parts.length > 1 && p.parts[1].indexOf('line') !== -1) {
                flags.dolayoutstyle = true;
            }
            else if(p.parts.length > 1 && p.parts[1] === 'mirror') {
                flags.doticks = flags.dolayoutstyle = true;
            }
            else if(ai === 'margin.pad') {
                flags.doticks = flags.dolayoutstyle = true;
            }
            else if(p.parts[0] === 'margin' ||
                    p.parts[1] === 'autorange' ||
                    p.parts[1] === 'rangemode' ||
                    p.parts[1] === 'type' ||
                    p.parts[1] === 'domain' ||
                    ai.match(/^(bar|box|font)/)) {
                flags.docalc = true;
            }
            /*
             * hovermode and dragmode don't need any redrawing, since they just
             * affect reaction to user input, everything else, assume full replot.
             * height, width, autosize get dealt with below. Except for the case of
             * of subplots - scenes - which require scene.updateFx to be called.
             */
            else if(['hovermode', 'dragmode'].indexOf(ai) !== -1) flags.domodebar = true;
            else if(['hovermode', 'dragmode', 'height',
                    'width', 'autosize'].indexOf(ai) === -1) {
                flags.doplot = true;
            }

            p.set(vi);
        }
    }

    // calculate autosizing - if size hasn't changed,
    // will remove h&w so we don't need to redraw
    if(aobj.autosize) aobj = plotAutoSize(gd, aobj);
    if(aobj.height || aobj.width || aobj.autosize) flags.docalc = true;

    if(flags.doplot || flags.docalc) {
        flags.layoutReplot = true;
    }

    // now all attribute mods are done, as are
    // redo and undo so we can save them

    return {
        flags: flags,
        undoit: undoit,
        redoit: redoit,
        eventData: Lib.extendDeep({}, redoit)
    };
}

/**
 * update: update trace and layout attributes of an existing plot
 *
 * @param {String | HTMLDivElement} gd
 *  the id or DOM element of the graph container div
 * @param {Object} traceUpdate
 *  attribute object `{astr1: val1, astr2: val2 ...}`
 *  corresponding to updates in the plot's traces
 * @param {Object} layoutUpdate
 *  attribute object `{astr1: val1, astr2: val2 ...}`
 *  corresponding to updates in the plot's layout
 * @param {Number[] | Number} [traces]
 *  integer or array of integers for the traces to alter (all if omitted)
 *
 */
Plotly.update = function update(gd, traceUpdate, layoutUpdate, traces) {
    gd = helpers.getGraphDiv(gd);
    helpers.clearPromiseQueue(gd);

    if(gd.framework && gd.framework.isPolar) {
        return Promise.resolve(gd);
    }

    if(!Lib.isPlainObject(traceUpdate)) traceUpdate = {};
    if(!Lib.isPlainObject(layoutUpdate)) layoutUpdate = {};

    if(Object.keys(traceUpdate).length) gd.changed = true;
    if(Object.keys(layoutUpdate).length) gd.changed = true;

    var restyleSpecs = _restyle(gd, traceUpdate, traces),
        restyleFlags = restyleSpecs.flags;

    var relayoutSpecs = _relayout(gd, layoutUpdate),
        relayoutFlags = relayoutSpecs.flags;

    // clear calcdata if required
    if(restyleFlags.clearCalc || relayoutFlags.docalc) gd.calcdata = undefined;

    // fill in redraw sequence
    var seq = [];

    if(restyleFlags.fullReplot && relayoutFlags.layoutReplot) {
        var layout = gd.layout;
        gd.layout = undefined;
        seq.push(function() { return Plotly.plot(gd, gd.data, layout); });
    }
    else if(restyleFlags.fullReplot) {
        seq.push(Plotly.plot);
    }
    else if(relayoutFlags.layoutReplot) {
        seq.push(subroutines.layoutReplot);
    }
    else {
        seq.push(Plots.previousPromises);
        Plots.supplyDefaults(gd);

        if(restyleFlags.dostyle) seq.push(subroutines.doTraceStyle);
        if(restyleFlags.docolorbars) seq.push(subroutines.doColorBars);
        if(relayoutFlags.dolegend) seq.push(subroutines.doLegend);
        if(relayoutFlags.dolayoutstyle) seq.push(subroutines.layoutStyles);
        if(relayoutFlags.doticks) seq.push(subroutines.doTicksRelayout);
        if(relayoutFlags.domodebar) seq.push(subroutines.doModeBar);
    }

    Queue.add(gd,
        update, [gd, restyleSpecs.undoit, relayoutSpecs.undoit, restyleSpecs.traces],
        update, [gd, restyleSpecs.redoit, relayoutSpecs.redoit, restyleSpecs.traces]
    );

    var plotDone = Lib.syncOrAsync(seq, gd);
    if(!plotDone || !plotDone.then) plotDone = Promise.resolve(gd);

    return plotDone.then(function() {
        subroutines.setRangeSliderRange(gd, relayoutSpecs.eventData);

        gd.emit('plotly_update', {
            data: restyleSpecs.eventData,
            layout: relayoutSpecs.eventData
        });

        return gd;
    });
};

/**
 * Animate to a frame, sequence of frame, frame group, or frame definition
 *
 * @param {string id or DOM element} gd
 *      the id or DOM element of the graph container div
 *
 * @param {string or object or array of strings or array of objects} frameOrGroupNameOrFrameList
 *      a single frame, array of frames, or group to which to animate. The intent is
 *      inferred by the type of the input. Valid inputs are:
 *
 *      - string, e.g. 'groupname': animate all frames of a given `group` in the order
 *            in which they are defined via `Plotly.addFrames`.
 *
 *      - array of strings, e.g. ['frame1', frame2']: a list of frames by name to which
 *            to animate in sequence
 *
 *      - object: {data: ...}: a frame definition to which to animate. The frame is not
 *            and does not need to be added via `Plotly.addFrames`. It may contain any of
 *            the properties of a frame, including `data`, `layout`, and `traces`. The
 *            frame is used as provided and does not use the `baseframe` property.
 *
 *      - array of objects, e.g. [{data: ...}, {data: ...}]: a list of frame objects,
 *            each following the same rules as a single `object`.
 *
 * @param {object} animationOpts
 *      configuration for the animation
 */
Plotly.animate = function(gd, frameOrGroupNameOrFrameList, animationOpts) {
    gd = helpers.getGraphDiv(gd);

    if(!Lib.isPlotDiv(gd)) {
        throw new Error('This element is not a Plotly plot: ' + gd);
    }

    var trans = gd._transitionData;

    // This is the queue of frames that will be animated as soon as possible. They
    // are popped immediately upon the *start* of a transition:
    if(!trans._frameQueue) {
        trans._frameQueue = [];
    }

    animationOpts = Plots.supplyAnimationDefaults(animationOpts);
    var transitionOpts = animationOpts.transition;
    var frameOpts = animationOpts.frame;

    // Since frames are popped immediately, an empty queue only means all frames have
    // *started* to transition, not that the animation is complete. To solve that,
    // track a separate counter that increments at the same time as frames are added
    // to the queue, but decrements only when the transition is complete.
    if(trans._frameWaitingCnt === undefined) {
        trans._frameWaitingCnt = 0;
    }

    function getTransitionOpts(i) {
        if(Array.isArray(transitionOpts)) {
            if(i >= transitionOpts.length) {
                return transitionOpts[0];
            } else {
                return transitionOpts[i];
            }
        } else {
            return transitionOpts;
        }
    }

    function getFrameOpts(i) {
        if(Array.isArray(frameOpts)) {
            if(i >= frameOpts.length) {
                return frameOpts[0];
            } else {
                return frameOpts[i];
            }
        } else {
            return frameOpts;
        }
    }

    return new Promise(function(resolve, reject) {
        function discardExistingFrames() {
            if(trans._frameQueue.length === 0) {
                return;
            }

            while(trans._frameQueue.length) {
                var next = trans._frameQueue.pop();
                if(next.onInterrupt) {
                    next.onInterrupt();
                }
            }

            gd.emit('plotly_animationinterrupted', []);
        }

        function queueFrames(frameList) {
            if(frameList.length === 0) return;

            for(var i = 0; i < frameList.length; i++) {
                var computedFrame;

                if(frameList[i].name) {
                    // If it's a named frame, compute it:
                    computedFrame = Plots.computeFrame(gd, frameList[i].name);
                } else {
                    // Otherwise we must have been given a simple object, so treat
                    // the input itself as the computed frame.
                    computedFrame = frameList[i].frame;
                }

                var frameOpts = getFrameOpts(i);
                var transitionOpts = getTransitionOpts(i);

                // It doesn't make much sense for the transition duration to be greater than
                // the frame duration, so limit it:
                transitionOpts.duration = Math.min(transitionOpts.duration, frameOpts.duration);

                var nextFrame = {
                    frame: computedFrame,
                    name: frameList[i].name,
                    frameOpts: frameOpts,
                    transitionOpts: transitionOpts,
                };

                if(i === frameList.length - 1) {
                    // The last frame in this .animate call stores the promise resolve
                    // and reject callbacks. This is how we ensure that the animation
                    // loop (which may exist as a result of a *different* .animate call)
                    // still resolves or rejecdts this .animate call's promise. once it's
                    // complete.
                    nextFrame.onComplete = resolve;
                    nextFrame.onInterrupt = reject;
                }

                trans._frameQueue.push(nextFrame);
            }

            // Set it as never having transitioned to a frame. This will cause the animation
            // loop to immediately transition to the next frame (which, for immediate mode,
            // is the first frame in the list since all others would have been discarded
            // below)
            if(animationOpts.mode === 'immediate') {
                trans._lastFrameAt = -Infinity;
            }

            // Only it's not already running, start a RAF loop. This could be avoided in the
            // case that there's only one frame, but it significantly complicated the logic
            // and only sped things up by about 5% or so for a lorenz attractor simulation.
            // It would be a fine thing to implement, but the benefit of that optimization
            // doesn't seem worth the extra complexity.
            if(!trans._animationRaf) {
                beginAnimationLoop();
            }
        }

        function stopAnimationLoop() {
            gd.emit('plotly_animated');

            // Be sure to unset also since it's how we know whether a loop is already running:
            window.cancelAnimationFrame(trans._animationRaf);
            trans._animationRaf = null;
        }

        function nextFrame() {
            if(trans._currentFrame && trans._currentFrame.onComplete) {
                // Execute the callback and unset it to ensure it doesn't
                // accidentally get called twice
                trans._currentFrame.onComplete();
                trans._currentFrame.onComplete = null;
            }

            var newFrame = trans._currentFrame = trans._frameQueue.shift();

            if(newFrame) {
                trans._lastFrameAt = Date.now();
                trans._timeToNext = newFrame.frameOpts.duration;

                // This is simply called and it's left to .transition to decide how to manage
                // interrupting current transitions. That means we don't need to worry about
                // how it resolves or what happens after this:
                Plots.transition(gd,
                    newFrame.frame.data,
                    newFrame.frame.layout,
                    helpers.coerceTraceIndices(gd, newFrame.frame.traces),
                    newFrame.frameOpts,
                    newFrame.transitionOpts
                );
            } else {
                // If there are no more frames, then stop the RAF loop:
                stopAnimationLoop();
            }
        }

        function beginAnimationLoop() {
            gd.emit('plotly_animating');

            // If no timer is running, then set last frame = long ago so that the next
            // frame is immediately transitioned:
            trans._lastFrameAt = -Infinity;
            trans._timeToNext = 0;
            trans._runningTransitions = 0;
            trans._currentFrame = null;

            var doFrame = function() {
                // This *must* be requested before nextFrame since nextFrame may decide
                // to cancel it if there's nothing more to animated:
                trans._animationRaf = window.requestAnimationFrame(doFrame);

                // Check if we're ready for a new frame:
                if(Date.now() - trans._lastFrameAt > trans._timeToNext) {
                    nextFrame();
                }
            };

            doFrame();
        }

        // This is an animate-local counter that helps match up option input list
        // items with the particular frame.
        var configCounter = 0;
        function setTransitionConfig(frame) {
            if(Array.isArray(transitionOpts)) {
                if(configCounter >= transitionOpts.length) {
                    frame.transitionOpts = transitionOpts[configCounter];
                } else {
                    frame.transitionOpts = transitionOpts[0];
                }
            } else {
                frame.transitionOpts = transitionOpts;
            }
            configCounter++;
            return frame;
        }

        // Disambiguate what's sort of frames have been received
        var i, frame;
        var frameList = [];
        var allFrames = frameOrGroupNameOrFrameList === undefined || frameOrGroupNameOrFrameList === null;
        var isFrameArray = Array.isArray(frameOrGroupNameOrFrameList);
        var isSingleFrame = !allFrames && !isFrameArray && Lib.isPlainObject(frameOrGroupNameOrFrameList);

        if(isSingleFrame) {
            frameList.push(setTransitionConfig({
                frame: Lib.extendFlat({}, frameOrGroupNameOrFrameList)
            }));
        } else if(allFrames || typeof frameOrGroupNameOrFrameList === 'string') {
            for(i = 0; i < trans._frames.length; i++) {
                frame = trans._frames[i];

                if(allFrames || frame.group === frameOrGroupNameOrFrameList) {
                    frameList.push(setTransitionConfig({name: frame.name}));
                }
            }
        } else if(isFrameArray) {
            for(i = 0; i < frameOrGroupNameOrFrameList.length; i++) {
                var frameOrName = frameOrGroupNameOrFrameList[i];
                if(typeof frameOrName === 'string') {
                    frameList.push(setTransitionConfig({name: frameOrName}));
                } else {
                    frameList.push(setTransitionConfig({
                        frame: Lib.extendFlat({}, frameOrName)
                    }));
                }
            }
        }

        // Verify that all of these frames actually exist; return and reject if not:
        for(i = 0; i < frameList.length; i++) {
            if(frameList[i].name && !trans._frameHash[frameList[i].name]) {
                Lib.warn('animate failure: frame not found: "' + frameList[i].name + '"');
                reject();
                return;
            }
        }

        // If the mode is either next or immediate, then all currently queued frames must
        // be dumped and the corresponding .animate promises rejected.
        if(['next', 'immediate'].indexOf(animationOpts.mode) !== -1) {
            discardExistingFrames();
        }

        if(frameList.length > 0) {
            queueFrames(frameList);
        } else {
            // This is the case where there were simply no frames. It's a little strange
            // since there's not much to do:
            gd.emit('plotly_animated');
            resolve();
        }
    });
};

/**
 * Register new frames
 *
 * @param {string id or DOM element} gd
 *      the id or DOM element of the graph container div
 *
 * @param {array of objects} frameList
 *      list of frame definitions, in which each object includes any of:
 *      - name: {string} name of frame to add
 *      - data: {array of objects} trace data
 *      - layout {object} layout definition
 *      - traces {array} trace indices
 *      - baseframe {string} name of frame from which this frame gets defaults
 *
 *  @param {array of integers) indices
 *      an array of integer indices matching the respective frames in `frameList`. If not
 *      provided, an index will be provided in serial order. If already used, the frame
 *      will be overwritten.
 */
Plotly.addFrames = function(gd, frameList, indices) {
    gd = helpers.getGraphDiv(gd);

    if(!Lib.isPlotDiv(gd)) {
        throw new Error('This element is not a Plotly plot: ' + gd);
    }

    var i, frame, j, idx;
    var _frames = gd._transitionData._frames;
    var _hash = gd._transitionData._frameHash;


    if(!Array.isArray(frameList)) {
        throw new Error('addFrames failure: frameList must be an Array of frame definitions' + frameList);
    }

    // Create a sorted list of insertions since we run into lots of problems if these
    // aren't in ascending order of index:
    //
    // Strictly for sorting. Make sure this is guaranteed to never collide with any
    // already-exisisting indices:
    var bigIndex = _frames.length + frameList.length * 2;

    var insertions = [];
    for(i = frameList.length - 1; i >= 0; i--) {
        insertions.push({
            frame: Plots.supplyFrameDefaults(frameList[i]),
            index: (indices && indices[i] !== undefined && indices[i] !== null) ? indices[i] : bigIndex + i
        });
    }

    // Sort this, taking note that undefined insertions end up at the end:
    insertions.sort(function(a, b) {
        if(a.index > b.index) return -1;
        if(a.index < b.index) return 1;
        return 0;
    });

    var ops = [];
    var revops = [];
    var frameCount = _frames.length;

    for(i = insertions.length - 1; i >= 0; i--) {
        frame = insertions[i].frame;

        if(!frame.name) {
            // Repeatedly assign a default name, incrementing the counter each time until
            // we get a name that's not in the hashed lookup table:
            while(_hash[(frame.name = 'frame ' + gd._transitionData._counter++)]);
        }

        if(_hash[frame.name]) {
            // If frame is present, overwrite its definition:
            for(j = 0; j < _frames.length; j++) {
                if(_frames[j].name === frame.name) break;
            }
            ops.push({type: 'replace', index: j, value: frame});
            revops.unshift({type: 'replace', index: j, value: _frames[j]});
        } else {
            // Otherwise insert it at the end of the list:
            idx = Math.max(0, Math.min(insertions[i].index, frameCount));

            ops.push({type: 'insert', index: idx, value: frame});
            revops.unshift({type: 'delete', index: idx});
            frameCount++;
        }
    }

    var undoFunc = Plots.modifyFrames,
        redoFunc = Plots.modifyFrames,
        undoArgs = [gd, revops],
        redoArgs = [gd, ops];

    if(Queue) Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs);

    return Plots.modifyFrames(gd, ops);
};

/**
 * Delete frame
 *
 * @param {string id or DOM element} gd
 *      the id or DOM element of the graph container div
 *
 * @param {array of integers} frameList
 *      list of integer indices of frames to be deleted
 */
Plotly.deleteFrames = function(gd, frameList) {
    gd = helpers.getGraphDiv(gd);

    if(!Lib.isPlotDiv(gd)) {
        throw new Error('This element is not a Plotly plot: ' + gd);
    }

    var i, idx;
    var _frames = gd._transitionData._frames;
    var ops = [];
    var revops = [];

    frameList = frameList.slice(0);
    frameList.sort();

    for(i = frameList.length - 1; i >= 0; i--) {
        idx = frameList[i];
        ops.push({type: 'delete', index: idx});
        revops.unshift({type: 'insert', index: idx, value: _frames[idx]});
    }

    var undoFunc = Plots.modifyFrames,
        redoFunc = Plots.modifyFrames,
        undoArgs = [gd, revops],
        redoArgs = [gd, ops];

    if(Queue) Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs);

    return Plots.modifyFrames(gd, ops);
};

/**
 * Purge a graph container div back to its initial pre-Plotly.plot state
 *
 * @param {string id or DOM element} gd
 *      the id or DOM element of the graph container div
 */
Plotly.purge = function purge(gd) {
    gd = helpers.getGraphDiv(gd);

    var fullLayout = gd._fullLayout || {},
        fullData = gd._fullData || [];

    // remove gl contexts
    Plots.cleanPlot([], {}, fullData, fullLayout);

    // purge properties
    Plots.purge(gd);

    // purge event emitter methods
    Events.purge(gd);

    // remove plot container
    if(fullLayout._container) fullLayout._container.remove();

    delete gd._context;
    delete gd._replotPending;
    delete gd._mouseDownTime;
    delete gd._hmpixcount;
    delete gd._hmlumcount;

    return gd;
};

/**
 * Reduce all reserved margin objects to a single required margin reservation.
 *
 * @param {Object} margins
 * @returns {{left: number, right: number, bottom: number, top: number}}
 */
function calculateReservedMargins(margins) {
    var resultingMargin = {left: 0, right: 0, bottom: 0, top: 0},
        marginName;

    if(margins) {
        for(marginName in margins) {
            if(margins.hasOwnProperty(marginName)) {
                resultingMargin.left += margins[marginName].left || 0;
                resultingMargin.right += margins[marginName].right || 0;
                resultingMargin.bottom += margins[marginName].bottom || 0;
                resultingMargin.top += margins[marginName].top || 0;
            }
        }
    }
    return resultingMargin;
}

function plotAutoSize(gd, aobj) {
    var fullLayout = gd._fullLayout,
        context = gd._context,
        computedStyle;

    var newHeight, newWidth;

    gd.emit('plotly_autosize');

    // embedded in an iframe - just take the full iframe size
    // if we get to this point, with no aspect ratio restrictions
    if(gd._context.fillFrame) {
        newWidth = window.innerWidth;
        newHeight = window.innerHeight;

        // somehow we get a few extra px height sometimes...
        // just hide it
        document.body.style.overflow = 'hidden';
    }
    else if(isNumeric(context.frameMargins) && context.frameMargins > 0) {
        var reservedMargins = calculateReservedMargins(gd._boundingBoxMargins),
            reservedWidth = reservedMargins.left + reservedMargins.right,
            reservedHeight = reservedMargins.bottom + reservedMargins.top,
            gdBB = fullLayout._container.node().getBoundingClientRect(),
            factor = 1 - 2 * context.frameMargins;

        newWidth = Math.round(factor * (gdBB.width - reservedWidth));
        newHeight = Math.round(factor * (gdBB.height - reservedHeight));
    }
    else {
        // plotly.js - let the developers do what they want, either
        // provide height and width for the container div,
        // specify size in layout, or take the defaults,
        // but don't enforce any ratio restrictions
        computedStyle = window.getComputedStyle(gd);
        newHeight = parseFloat(computedStyle.height) || fullLayout.height;
        newWidth = parseFloat(computedStyle.width) || fullLayout.width;
    }

    if(Math.abs(fullLayout.width - newWidth) > 1 ||
            Math.abs(fullLayout.height - newHeight) > 1) {
        fullLayout.height = gd.layout.height = newHeight;
        fullLayout.width = gd.layout.width = newWidth;
    }
    // if there's no size change, update layout but
    // delete the autosize attr so we don't redraw
    // but can't call layoutStyles for initial autosize
    else if(fullLayout.autosize !== 'initial') {
        delete(aobj.autosize);
        fullLayout.autosize = gd.layout.autosize = true;
    }

    Plots.sanitizeMargins(fullLayout);

    return aobj;
}

// -------------------------------------------------------
// makePlotFramework: Create the plot container and axes
// -------------------------------------------------------
function makePlotFramework(gd) {
    var gd3 = d3.select(gd),
        fullLayout = gd._fullLayout;

    // Plot container
    fullLayout._container = gd3.selectAll('.plot-container').data([0]);
    fullLayout._container.enter().insert('div', ':first-child')
        .classed('plot-container', true)
        .classed('plotly', true);

    // Make the svg container
    fullLayout._paperdiv = fullLayout._container.selectAll('.svg-container').data([0]);
    fullLayout._paperdiv.enter().append('div')
        .classed('svg-container', true)
        .style('position', 'relative');

    // Initial autosize
    if(fullLayout.autosize === 'initial') {
        plotAutoSize(gd, {});
        fullLayout.autosize = true;
        gd.layout.autosize = true;
    }

    // Make the graph containers
    // start fresh each time we get here, so we know the order comes out
    // right, rather than enter/exit which can muck up the order
    // TODO: sort out all the ordering so we don't have to
    // explicitly delete anything
    fullLayout._glcontainer = fullLayout._paperdiv.selectAll('.gl-container')
        .data([0]);
    fullLayout._glcontainer.enter().append('div')
        .classed('gl-container', true);

    fullLayout._geocontainer = fullLayout._paperdiv.selectAll('.geo-container')
        .data([0]);
    fullLayout._geocontainer.enter().append('div')
        .classed('geo-container', true);

    fullLayout._paperdiv.selectAll('.main-svg').remove();

    fullLayout._paper = fullLayout._paperdiv.insert('svg', ':first-child')
        .classed('main-svg', true);

    fullLayout._toppaper = fullLayout._paperdiv.append('svg')
        .classed('main-svg', true);

    if(!fullLayout._uid) {
        var otherUids = [];
        d3.selectAll('defs').each(function() {
            if(this.id) otherUids.push(this.id.split('-')[1]);
        });
        fullLayout._uid = Lib.randstr(otherUids);
    }

    fullLayout._paperdiv.selectAll('.main-svg')
        .attr(xmlnsNamespaces.svgAttrs);

    fullLayout._defs = fullLayout._paper.append('defs')
        .attr('id', 'defs-' + fullLayout._uid);

    fullLayout._topdefs = fullLayout._toppaper.append('defs')
        .attr('id', 'topdefs-' + fullLayout._uid);

    fullLayout._draggers = fullLayout._paper.append('g')
        .classed('draglayer', true);

    // lower shape layer
    // (only for shapes to be drawn below the whole plot)
    var layerBelow = fullLayout._paper.append('g')
        .classed('layer-below', true);
    fullLayout._imageLowerLayer = layerBelow.append('g')
        .classed('imagelayer', true);
    fullLayout._shapeLowerLayer = layerBelow.append('g')
        .classed('shapelayer', true);

    // single cartesian layer for the whole plot
    fullLayout._cartesianlayer = fullLayout._paper.append('g').classed('cartesianlayer', true);

    // single ternary layer for the whole plot
    fullLayout._ternarylayer = fullLayout._paper.append('g').classed('ternarylayer', true);

    // upper shape layer
    // (only for shapes to be drawn above the whole plot, including subplots)
    var layerAbove = fullLayout._paper.append('g')
        .classed('layer-above', true);
    fullLayout._imageUpperLayer = layerAbove.append('g')
        .classed('imagelayer', true);
    fullLayout._shapeUpperLayer = layerAbove.append('g')
        .classed('shapelayer', true);

    // single pie layer for the whole plot
    fullLayout._pielayer = fullLayout._paper.append('g').classed('pielayer', true);

    // fill in image server scrape-svg
    fullLayout._glimages = fullLayout._paper.append('g').classed('glimages', true);
    fullLayout._geoimages = fullLayout._paper.append('g').classed('geoimages', true);

    // lastly info (legend, annotations) and hover layers go on top
    // these are in a different svg element normally, but get collapsed into a single
    // svg when exporting (after inserting 3D)
    fullLayout._infolayer = fullLayout._toppaper.append('g').classed('infolayer', true);
    fullLayout._zoomlayer = fullLayout._toppaper.append('g').classed('zoomlayer', true);
    fullLayout._hoverlayer = fullLayout._toppaper.append('g').classed('hoverlayer', true);

    gd.emit('plotly_framework');

    return 'FRAMEWORK';
}