'use strict'; var d3 = require('@plotly/d3'); var isNumeric = require('fast-isnumeric'); var hasHover = require('has-hover'); var Lib = require('../lib'); var nestedProperty = Lib.nestedProperty; var Events = require('../lib/events'); var Queue = require('../lib/queue'); var Registry = require('../registry'); var PlotSchema = require('./plot_schema'); var Plots = require('../plots/plots'); var Axes = require('../plots/cartesian/axes'); var handleRangeDefaults = require('../plots/cartesian/range_defaults'); var cartesianLayoutAttributes = require('../plots/cartesian/layout_attributes'); var Drawing = require('../components/drawing'); var Color = require('../components/color'); var initInteractions = require('../plots/cartesian/graph_interact').initInteractions; var xmlnsNamespaces = require('../constants/xmlns_namespaces'); var clearOutline = require('../components/selections').clearOutline; var dfltConfig = require('./plot_config').dfltConfig; var manageArrays = require('./manage_arrays'); var helpers = require('./helpers'); var subroutines = require('./subroutines'); var editTypes = require('./edit_types'); var AX_NAME_PATTERN = require('../plots/cartesian/constants').AX_NAME_PATTERN; var numericNameWarningCount = 0; var numericNameWarningCountLimit = 5; /** * Internal plot-creation function * * @param {string id or DOM element} gd * the id or DOM element of the graph container div * @param {array of objects} data * array of traces, containing the data and display information for each trace * @param {object} layout * object describing the overall display of the plot, * all the stuff that doesn't pertain to any individual trace * @param {object} config * configuration options (see ./plot_config.js for more info) * * OR * * @param {string id or DOM element} gd * the id or DOM element of the graph container div * @param {object} figure * object containing `data`, `layout`, `config`, and `frames` members * */ function _doPlot(gd, data, layout, config) { var frames; gd = Lib.getGraphDiv(gd); // Events.init is idempotent and bails early if gd has already been init'd Events.init(gd); if(Lib.isPlainObject(data)) { var obj = data; data = obj.data; layout = obj.layout; config = obj.config; frames = obj.frames; } var okToPlot = Events.triggerHandler(gd, 'plotly_beforeplot', [data, layout, config]); if(okToPlot === false) return Promise.reject(); // if there's no data or layout, and this isn't yet a plotly plot // container, log a warning to help plotly.js users debug if(!data && !layout && !Lib.isPlotDiv(gd)) { Lib.warn('Calling _doPlot as if redrawing ' + 'but this container doesn\'t yet have a plot.', gd); } function addFrames() { if(frames) { return exports.addFrames(gd, frames); } } // transfer configuration options to gd until we move over to // a more OO like model setPlotContext(gd, config); if(!layout) layout = {}; // hook class for plots main container (in case of plotly.js // this won't be #embedded-graph or .js-tab-contents) d3.select(gd).classed('js-plotly-plot', true); // off-screen getBoundingClientRect testing space, // in #js-plotly-tester (and stored as Drawing.tester) // so we can share cached text across tabs Drawing.makeTester(); // collect promises for any async actions during plotting // any part of the plotting code can push to gd._promises, then // before we move to the next step, we check that they're all // complete, and empty out the promise list again. if(!Array.isArray(gd._promises)) gd._promises = []; var graphWasEmpty = ((gd.data || []).length === 0 && Array.isArray(data)); // if there is already data on the graph, append the new data // if you only want to redraw, pass a non-array for data if(Array.isArray(data)) { helpers.cleanData(data); if(graphWasEmpty) gd.data = data; else gd.data.push.apply(gd.data, data); // for routines outside graph_obj that want a clean tab // (rather than appending to an existing one) gd.empty // is used to determine whether to make a new tab gd.empty = false; } if(!gd.layout || graphWasEmpty) { gd.layout = helpers.cleanLayout(layout); } Plots.supplyDefaults(gd); var fullLayout = gd._fullLayout; var hasCartesian = fullLayout._has('cartesian'); // so we don't try to re-call _doPlot from inside // legend and colorbar, if margins changed fullLayout._replotting = true; // make or remake the framework if we need to if(graphWasEmpty || fullLayout._shouldCreateBgLayer) { makePlotFramework(gd); if(fullLayout._shouldCreateBgLayer) { delete fullLayout._shouldCreateBgLayer; } } // clear gradient and pattern defs on each .plot call, because we know we'll loop through all traces Drawing.initGradients(gd); Drawing.initPatterns(gd); // save initial show spikes once per graph if(graphWasEmpty) Axes.saveShowSpikeInitial(gd); // prepare the data and find the autorange // generate calcdata, if we need to // to force redoing calcdata, just delete it before calling _doPlot var recalc = !gd.calcdata || gd.calcdata.length !== (gd._fullData || []).length; if(recalc) Plots.doCalcdata(gd); // in case it has changed, attach fullData traces to calcdata for(var i = 0; i < gd.calcdata.length; i++) { gd.calcdata[i][0].trace = gd._fullData[i]; } // make the figure responsive if(gd._context.responsive) { if(!gd._responsiveChartHandler) { // Keep a reference to the resize handler to purge it down the road gd._responsiveChartHandler = function() { if(!Lib.isHidden(gd)) Plots.resize(gd); }; // Listen to window resize window.addEventListener('resize', gd._responsiveChartHandler); } } else { Lib.clearResponsive(gd); } /* * start async-friendly code - now we're actually drawing things */ var oldMargins = Lib.extendFlat({}, fullLayout._size); // draw framework first so that margin-pushing // components can position themselves correctly var drawFrameworkCalls = 0; function drawFramework() { var basePlotModules = fullLayout._basePlotModules; for(var i = 0; i < basePlotModules.length; i++) { if(basePlotModules[i].drawFramework) { basePlotModules[i].drawFramework(gd); } } if(!fullLayout._glcanvas && fullLayout._has('gl')) { fullLayout._glcanvas = fullLayout._glcontainer.selectAll('.gl-canvas').data([{ key: 'contextLayer', context: true, pick: false }, { key: 'focusLayer', context: false, pick: false }, { key: 'pickLayer', context: false, pick: true }], function(d) { return d.key; }); fullLayout._glcanvas.enter().append('canvas') .attr('class', function(d) { return 'gl-canvas gl-canvas-' + d.key.replace('Layer', ''); }) .style({ position: 'absolute', top: 0, left: 0, overflow: 'visible', 'pointer-events': 'none' }); } var plotGlPixelRatio = gd._context.plotGlPixelRatio; if(fullLayout._glcanvas) { fullLayout._glcanvas .attr('width', fullLayout.width * plotGlPixelRatio) .attr('height', fullLayout.height * plotGlPixelRatio) .style('width', fullLayout.width + 'px') .style('height', fullLayout.height + 'px'); var regl = fullLayout._glcanvas.data()[0].regl; if(regl) { // Unfortunately, this can happen when relayouting to large // width/height on some browsers. if(Math.floor(fullLayout.width * plotGlPixelRatio) !== regl._gl.drawingBufferWidth || Math.floor(fullLayout.height * plotGlPixelRatio) !== regl._gl.drawingBufferHeight ) { var msg = 'WebGL context buffer and canvas dimensions do not match due to browser/WebGL bug.'; if(drawFrameworkCalls) { Lib.error(msg); } else { Lib.log(msg + ' Clearing graph and plotting again.'); Plots.cleanPlot([], {}, gd._fullData, fullLayout); Plots.supplyDefaults(gd); fullLayout = gd._fullLayout; Plots.doCalcdata(gd); drawFrameworkCalls++; return drawFramework(); } } } } if(fullLayout.modebar.orientation === 'h') { fullLayout._modebardiv .style('height', null) .style('width', '100%'); } else { fullLayout._modebardiv .style('width', null) .style('height', fullLayout.height + 'px'); } return Plots.previousPromises(gd); } // draw anything that can affect margins. function marginPushers() { // First reset the list of things that are allowed to change the margins // So any deleted traces or components will be wiped out of the // automargin calculation. // This means *every* margin pusher must be listed here, even if it // doesn't actually try to push the margins until later. Plots.clearAutoMarginIds(gd); subroutines.drawMarginPushers(gd); Axes.allowAutoMargin(gd); if(gd._fullLayout.title.text && gd._fullLayout.title.automargin) Plots.allowAutoMargin(gd, 'title.automargin'); // TODO can this be moved elsewhere? if(fullLayout._has('pie')) { var fullData = gd._fullData; for(var i = 0; i < fullData.length; i++) { var trace = fullData[i]; if(trace.type === 'pie' && trace.automargin) { Plots.allowAutoMargin(gd, 'pie.' + trace.uid + '.automargin'); } } } Plots.doAutoMargin(gd); return Plots.previousPromises(gd); } // in case the margins changed, draw margin pushers again function marginPushersAgain() { if(!Plots.didMarginChange(oldMargins, fullLayout._size)) return; return Lib.syncOrAsync([ marginPushers, subroutines.layoutStyles ], gd); } function positionAndAutorange() { if(!recalc) { doAutoRangeAndConstraints(); return; } // TODO: autosize extra for text markers and images // see https://github.com/plotly/plotly.js/issues/1111 return Lib.syncOrAsync([ Registry.getComponentMethod('shapes', 'calcAutorange'), Registry.getComponentMethod('annotations', 'calcAutorange'), doAutoRangeAndConstraints ], gd); } function doAutoRangeAndConstraints() { if(gd._transitioning) return; subroutines.doAutoRangeAndConstraints(gd); // store initial ranges *after* enforcing constraints, otherwise // we will never look like we're at the initial ranges if(graphWasEmpty) Axes.saveRangeInitial(gd); // this one is different from shapes/annotations calcAutorange // the others incorporate those components into ax._extremes, // this one actually sets the ranges in rangesliders. Registry.getComponentMethod('rangeslider', 'calcAutorange')(gd); } // draw ticks, titles, and calculate axis scaling (._b, ._m) function drawAxes() { return Axes.draw(gd, graphWasEmpty ? '' : 'redraw'); } var seq = [ Plots.previousPromises, addFrames, drawFramework, marginPushers, marginPushersAgain ]; if(hasCartesian) seq.push(positionAndAutorange); seq.push(subroutines.layoutStyles); if(hasCartesian) { seq.push( drawAxes, function insideTickLabelsAutorange(gd) { var insideTickLabelsUpdaterange = gd._fullLayout._insideTickLabelsUpdaterange; if(insideTickLabelsUpdaterange) { gd._fullLayout._insideTickLabelsUpdaterange = undefined; return relayout(gd, insideTickLabelsUpdaterange).then(function() { Axes.saveRangeInitial(gd, true); }); } } ); } seq.push( subroutines.drawData, subroutines.finalDraw, initInteractions, Plots.addLinks, Plots.rehover, Plots.redrag, Plots.reselect, // TODO: doAutoMargin is only needed here for axis automargin, which // happens outside of marginPushers where all the other automargins are // calculated. Would be much better to separate margin calculations from // component drawing - see https://github.com/plotly/plotly.js/issues/2704 Plots.doAutoMargin, Plots.previousPromises ); // even if everything we did was synchronous, return a promise // so that the caller doesn't care which route we took var plotDone = Lib.syncOrAsync(seq, gd); if(!plotDone || !plotDone.then) plotDone = Promise.resolve(); return plotDone.then(function() { emitAfterPlot(gd); return gd; }); } function emitAfterPlot(gd) { var fullLayout = gd._fullLayout; if(fullLayout._redrawFromAutoMarginCount) { fullLayout._redrawFromAutoMarginCount--; } else { gd.emit('plotly_afterplot'); } } function setPlotConfig(obj) { return Lib.extendFlat(dfltConfig, obj); } function setBackground(gd, bgColor) { try { gd._fullLayout._paper.style('background', bgColor); } catch(e) { Lib.error(e); } } function opaqueSetBackground(gd, bgColor) { var blend = Color.combine(bgColor, 'white'); setBackground(gd, blend); } function setPlotContext(gd, config) { if(!gd._context) { gd._context = Lib.extendDeep({}, dfltConfig); // stash href, used to make robust clipPath URLs var base = d3.select('base'); gd._context._baseUrl = base.size() && base.attr('href') ? window.location.href.split('#')[0] : ''; } var context = gd._context; var i, keys, key; if(config) { keys = Object.keys(config); for(i = 0; i < keys.length; i++) { key = keys[i]; if(key === 'editable' || key === 'edits') continue; 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; } // now deal with editable and edits - first editable overrides // everything, then edits refines var editable = config.editable; if(editable !== undefined) { // we're not going to *use* context.editable, we're only going to // use context.edits... but keep it for the record context.editable = editable; keys = Object.keys(context.edits); for(i = 0; i < keys.length; i++) { context.edits[keys[i]] = editable; } } if(config.edits) { keys = Object.keys(config.edits); for(i = 0; i < keys.length; i++) { key = keys[i]; if(key in context.edits) { context.edits[key] = config.edits[key]; } } } // not part of the user-facing config options context._exportedPlot = config._exportedPlot; } // staticPlot forces a bunch of others: if(context.staticPlot) { context.editable = false; context.edits = {}; context.autosizable = false; context.scrollZoom = false; context.doubleClick = false; context.showTips = false; context.showLink = false; context.displayModeBar = false; } // make sure hover-only devices have mode bar visible if(context.displayModeBar === 'hover' && !hasHover) { context.displayModeBar = true; } // default and fallback for setBackground if(context.setBackground === 'transparent' || typeof context.setBackground !== 'function') { context.setBackground = setBackground; } // Check if gd has a specified widht/height to begin with context._hasZeroHeight = context._hasZeroHeight || gd.clientHeight === 0; context._hasZeroWidth = context._hasZeroWidth || gd.clientWidth === 0; // fill context._scrollZoom helper to help manage scrollZoom flaglist var szIn = context.scrollZoom; var szOut = context._scrollZoom = {}; if(szIn === true) { szOut.cartesian = 1; szOut.gl3d = 1; szOut.geo = 1; szOut.mapbox = 1; szOut.map = 1; } else if(typeof szIn === 'string') { var parts = szIn.split('+'); for(i = 0; i < parts.length; i++) { szOut[parts[i]] = 1; } } else if(szIn !== false) { szOut.gl3d = 1; szOut.geo = 1; szOut.mapbox = 1; szOut.map = 1; } } // convenience function to force a full redraw, mostly for use by plotly.js function redraw(gd) { gd = Lib.getGraphDiv(gd); if(!Lib.isPlotDiv(gd)) { throw new Error('This element is not a Plotly plot: ' + gd); } helpers.cleanData(gd.data); helpers.cleanLayout(gd.layout); gd.calcdata = undefined; return exports._doPlot(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 */ function newPlot(gd, data, layout, config) { gd = Lib.getGraphDiv(gd); // remove gl contexts Plots.cleanPlot([], {}, gd._fullData || [], gd._fullLayout || {}); Plots.purge(gd); return exports._doPlot(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; var positiveIndices = []; var i; var 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); var 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 = 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(!Lib.isArrayOrTypedArray(insert)) { throw new Error('attribute: ' + key + ' index: ' + j + ' must be an array'); } if(!Lib.isArrayOrTypedArray(target)) { throw new Error('cannot extend missing or non-array attribute: ' + key); } if(target.constructor !== insert.constructor) { throw new Error('cannot extend array with an array of a different type: ' + 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} updateArray * @return {Object} */ function spliceTraces(gd, update, indices, maxPoints, updateArray) { assertExtendTracesArgs(gd, update, indices, maxPoints); var updateProps = getExtendProperties(gd, update, indices, maxPoints); var undoUpdate = {}; var undoPoints = {}; for(var i = 0; i < updateProps.length; i++) { var prop = updateProps[i].prop; var maxp = updateProps[i].maxp; // return new array and remainder var out = updateArray(updateProps[i].target, updateProps[i].insert, maxp); prop.set(out[0]); // build the inverse update object for the undo operation if(!Array.isArray(undoUpdate[prop.astr])) undoUpdate[prop.astr] = []; undoUpdate[prop.astr].push(out[1]); // build the matching maxPoints undo object containing original trace lengths if(!Array.isArray(undoPoints[prop.astr])) undoPoints[prop.astr] = []; undoPoints[prop.astr].push(updateProps[i].target.length); } return {update: undoUpdate, maxPoints: undoPoints}; } function concatTypedArray(arr0, arr1) { var arr2 = new arr0.constructor(arr0.length + arr1.length); arr2.set(arr0); arr2.set(arr1, arr0.length); return arr2; } /** * 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. * */ function extendTraces(gd, update, indices, maxPoints) { gd = Lib.getGraphDiv(gd); function updateArray(target, insert, maxp) { var newArray, remainder; if(Lib.isTypedArray(target)) { if(maxp < 0) { var none = new target.constructor(0); var both = concatTypedArray(target, insert); if(maxp < 0) { newArray = both; remainder = none; } else { newArray = none; remainder = both; } } else { newArray = new target.constructor(maxp); remainder = new target.constructor(target.length + insert.length - maxp); if(maxp === insert.length) { newArray.set(insert); remainder.set(target); } else if(maxp < insert.length) { var numberOfItemsFromInsert = insert.length - maxp; newArray.set(insert.subarray(numberOfItemsFromInsert)); remainder.set(target); remainder.set(insert.subarray(0, numberOfItemsFromInsert), target.length); } else { var numberOfItemsFromTarget = maxp - insert.length; var targetBegin = target.length - numberOfItemsFromTarget; newArray.set(target.subarray(targetBegin)); newArray.set(insert, numberOfItemsFromTarget); remainder.set(target.subarray(0, targetBegin)); } } } else { newArray = target.concat(insert); remainder = (maxp >= 0 && maxp < newArray.length) ? newArray.splice(0, newArray.length - maxp) : []; } return [newArray, remainder]; } var undo = spliceTraces(gd, update, indices, maxPoints, updateArray); var promise = exports.redraw(gd); var undoArgs = [gd, undo.update, indices, undo.maxPoints]; Queue.add(gd, exports.prependTraces, undoArgs, extendTraces, arguments); return promise; } function prependTraces(gd, update, indices, maxPoints) { gd = Lib.getGraphDiv(gd); function updateArray(target, insert, maxp) { var newArray, remainder; if(Lib.isTypedArray(target)) { if(maxp <= 0) { var none = new target.constructor(0); var both = concatTypedArray(insert, target); if(maxp < 0) { newArray = both; remainder = none; } else { newArray = none; remainder = both; } } else { newArray = new target.constructor(maxp); remainder = new target.constructor(target.length + insert.length - maxp); if(maxp === insert.length) { newArray.set(insert); remainder.set(target); } else if(maxp < insert.length) { var numberOfItemsFromInsert = insert.length - maxp; newArray.set(insert.subarray(0, numberOfItemsFromInsert)); remainder.set(insert.subarray(numberOfItemsFromInsert)); remainder.set(target, numberOfItemsFromInsert); } else { var numberOfItemsFromTarget = maxp - insert.length; newArray.set(insert); newArray.set(target.subarray(0, numberOfItemsFromTarget), insert.length); remainder.set(target.subarray(numberOfItemsFromTarget)); } } } else { newArray = insert.concat(target); remainder = (maxp >= 0 && maxp < newArray.length) ? newArray.splice(maxp, newArray.length) : []; } return [newArray, remainder]; } var undo = spliceTraces(gd, update, indices, maxPoints, updateArray); var promise = exports.redraw(gd); var undoArgs = [gd, undo.update, indices, undo.maxPoints]; Queue.add(gd, exports.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 * */ function addTraces(gd, traces, newIndices) { gd = Lib.getGraphDiv(gd); var currentIndices = []; var undoFunc = exports.deleteTraces; var redoFunc = addTraces; var undoArgs = [gd, currentIndices]; var redoArgs = [gd, traces]; // no newIndices here var i; var 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]; } // make sure traces do not repeat existing ones traces = traces.map(function(trace) { return Lib.extendFlat({}, trace); }); helpers.cleanData(traces); // add the traces to gd.data (no redrawing yet!) for(i = 0; i < traces.length; i++) { 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 = exports.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 = exports.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 */ function deleteTraces(gd, indices) { gd = Lib.getGraphDiv(gd); var traces = []; var undoFunc = exports.addTraces; var redoFunc = deleteTraces; var undoArgs = [gd, traces, indices]; var redoArgs = [gd, indices]; var i; var 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 = exports.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' */ function moveTraces(gd, currentIndices, newIndices) { gd = Lib.getGraphDiv(gd); var newData = []; var movingTraceMap = []; var undoFunc = moveTraces; var redoFunc = moveTraces; var undoArgs = [gd, newIndices, currentIndices]; var redoArgs = [gd, currentIndices, newIndices]; var 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 = exports.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). */ function restyle(gd, astr, val, _traces) { gd = Lib.getGraphDiv(gd); helpers.clearPromiseQueue(gd); var aobj = {}; if(typeof astr === 'string') aobj[astr] = val; else if(Lib.isPlainObject(astr)) { // the 3-arg form aobj = Lib.extendFlat({}, 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 traces = helpers.coerceTraceIndices(gd, _traces); var specs = _restyle(gd, aobj, traces); var flags = specs.flags; // clear calcdata and/or axis types if required so they get regenerated if(flags.calc) gd.calcdata = undefined; if(flags.clearAxisTypes) helpers.clearAxisTypes(gd, traces, {}); // fill in redraw sequence var seq = []; if(flags.fullReplot) { seq.push(exports._doPlot); } else { seq.push(Plots.previousPromises); // maybe only call Plots.supplyDataDefaults in the splom case, // to skip over long and slow axes defaults Plots.supplyDefaults(gd); if(flags.markerSize) { Plots.doCalcdata(gd); addAxRangeSequence(seq); // TODO // if all axes have autorange:false, then // proceed to subroutines.doTraceStyle(), // otherwise we must go through addAxRangeSequence, // which in general must redraws 'all' axes } if(flags.style) seq.push(subroutines.doTraceStyle); if(flags.colorbars) seq.push(subroutines.doColorBars); seq.push(emitAfterPlot); } seq.push( Plots.rehover, Plots.redrag, Plots.reselect ); 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; }); } // for undo: undefined initial vals must be turned into nulls // so that we unset rather than ignore them function undefinedToNull(val) { if(val === undefined) return null; return val; } /** * Factory function to wrap nestedProperty with GUI edits if necessary * with GUI edits we add an optional prefix to the nestedProperty constructor * to prepend to the attribute string in the preGUI store. */ function makeNP(preGUI, guiEditFlag) { if(!guiEditFlag) return nestedProperty; return function(container, attr, prefix) { var np = nestedProperty(container, attr); var npSet = np.set; np.set = function(val) { var fullAttr = (prefix || '') + attr; storeCurrent(fullAttr, np.get(), val, preGUI); npSet(val); }; return np; }; } function storeCurrent(attr, val, newVal, preGUI) { if(Array.isArray(val) || Array.isArray(newVal)) { var arrayVal = Array.isArray(val) ? val : []; var arrayNew = Array.isArray(newVal) ? newVal : []; var maxLen = Math.max(arrayVal.length, arrayNew.length); for(var i = 0; i < maxLen; i++) { storeCurrent(attr + '[' + i + ']', arrayVal[i], arrayNew[i], preGUI); } } else if(Lib.isPlainObject(val) || Lib.isPlainObject(newVal)) { var objVal = Lib.isPlainObject(val) ? val : {}; var objNew = Lib.isPlainObject(newVal) ? newVal : {}; var objBoth = Lib.extendFlat({}, objVal, objNew); for(var key in objBoth) { storeCurrent(attr + '.' + key, objVal[key], objNew[key], preGUI); } } else if(preGUI[attr] === undefined) { preGUI[attr] = undefinedToNull(val); } } /** * storeDirectGUIEdit: for routines that skip restyle/relayout and mock it * by emitting a plotly_restyle or plotly_relayout event, this routine * keeps track of the initial state in _preGUI for use by uirevision * Does *not* apply these changes to data/layout - that's the responsibility * of the calling routine. * * @param {object} container: the input attributes container (eg `layout` or a `trace`) * @param {object} preGUI: where original values should be stored, either * `layout._preGUI` or `layout._tracePreGUI[uid]` * @param {object} edits: the {attr: val} object as normally passed to `relayout` etc */ function _storeDirectGUIEdit(container, preGUI, edits) { for(var attr in edits) { var np = nestedProperty(container, attr); storeCurrent(attr, np.get(), edits[attr], preGUI); } } function _restyle(gd, aobj, traces) { var fullLayout = gd._fullLayout; var fullData = gd._fullData; var data = gd.data; var guiEditFlag = fullLayout._guiEditing; var layoutNP = makeNP(fullLayout._preGUI, guiEditFlag); var eventData = Lib.extendDeepAll({}, aobj); var i; cleanDeprecatedAttributeKeys(aobj); // initialize flags var flags = editTypes.traceFlags(); // copies of the change (and previous values of anything affected) // for the undo / redo queue var redoit = {}; var undoit = {}; var axlist; // 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 = 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'; } function getFullTrace(traceIndex) { // usually fullData maps 1:1 onto data, but with groupby transforms // the fullData index can be greater. Take the *first* matching trace. for(var j = traceIndex; j < fullData.length; j++) { if(fullData[j]._input === data[traceIndex]) return fullData[j]; } // should never get here - and if we *do* it should cause an error // later on undefined fullTrace is passed to nestedProperty. } // 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 || helpers.hasParent(aobj, attr)) return; var extraparam; if(attr.substr(0, 6) === 'LAYOUT') { extraparam = layoutNP(gd.layout, attr.replace('LAYOUT', '')); } else { var tracei = traces[i]; var preGUI = fullLayout._tracePreGUI[getFullTrace(tracei)._fullInput.uid]; extraparam = makeNP(preGUI, guiEditFlag)(data[tracei], attr); } if(!(attr in undoit)) { undoit[attr] = a0(); } if(undoit[attr][i] === undefined) { undoit[attr][i] = undefinedToNull(extraparam.get()); } if(val !== undefined) { extraparam.set(val); } } function allBins(binAttr) { return function(j) { return fullData[j][binAttr]; }; } function arrayBins(binAttr) { return function(vij, j) { return vij === false ? fullData[traces[j]][binAttr] : null; }; } // 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) { if(helpers.hasParent(aobj, ai)) { throw new Error('cannot set ' + ai + ' and a parent attribute simultaneously'); } var vi = aobj[ai]; var cont; var contFull; var param; var oldVal; var newVal; var valObject; // Backward compatibility shim for turning histogram autobin on, // or freezing previous autobinned values. // Replace obsolete `autobin(x|y): true` with `(x|y)bins: null` // and `autobin(x|y): false` with the `(x|y)bins` in `fullData` if(ai === 'autobinx' || ai === 'autobiny') { ai = ai.charAt(ai.length - 1) + 'bins'; if(Array.isArray(vi)) vi = vi.map(arrayBins(ai)); else if(vi === false) vi = traces.map(allBins(ai)); else vi = null; } redoit[ai] = vi; if(ai.substr(0, 6) === 'LAYOUT') { param = layoutNP(gd.layout, ai.replace('LAYOUT', '')); undoit[ai] = [undefinedToNull(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.calc = true; continue; } // set attribute in gd.data undoit[ai] = a0(); for(i = 0; i < traces.length; i++) { cont = data[traces[i]]; contFull = getFullTrace(traces[i]); var preGUI = fullLayout._tracePreGUI[contFull._fullInput.uid]; param = makeNP(preGUI, guiEditFlag)(cont, ai); oldVal = param.get(); newVal = Array.isArray(vi) ? vi[i % vi.length] : vi; if(newVal === undefined) continue; var finalPart = param.parts[param.parts.length - 1]; var prefix = ai.substr(0, ai.length - finalPart.length - 1); var prefixDot = prefix ? prefix + '.' : ''; var innerContFull = prefix ? nestedProperty(contFull, prefix).get() : contFull; valObject = PlotSchema.getTraceValObject(contFull, param.parts); if(valObject && valObject.impliedEdits && newVal !== null) { for(var impliedKey in valObject.impliedEdits) { doextra(Lib.relativeAttr(ai, impliedKey), valObject.impliedEdits[impliedKey], i); } } else if((finalPart === 'thicknessmode' || finalPart === 'lenmode') && oldVal !== newVal && (newVal === 'fraction' || newVal === 'pixels') && innerContFull ) { // 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 var gs = fullLayout._size; var orient = innerContFull.orient; var topOrBottom = (orient === 'top') || (orient === 'bottom'); if(finalPart === 'thicknessmode') { var thicknorm = topOrBottom ? gs.h : gs.w; doextra(prefixDot + 'thickness', innerContFull.thickness * (newVal === 'fraction' ? 1 / thicknorm : thicknorm), i); } else { var lennorm = topOrBottom ? gs.w : gs.h; doextra(prefixDot + 'len', innerContFull.len * (newVal === 'fraction' ? 1 / lennorm : lennorm), i); } } else if(ai === 'type' && ( (newVal === 'pie') !== (oldVal === 'pie') || (newVal === 'funnelarea') !== (oldVal === 'funnelarea') )) { var labelsTo = 'x'; var 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' || oldVal === 'funnelarea') { nestedProperty(cont, 'marker.color') .set(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')) { nestedProperty(cont, 'marker.colors') .set(nestedProperty(cont, 'marker.color').get()); } } undoit[ai][i] = undefinedToNull(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); // obnoxious that we need this level of coupling... but in order to // properly handle setting orientation to `null` we need to mimic // the logic inside Bars.supplyDefaults for default orientation var defaultOrientation = (cont.x && !cont.y) ? 'h' : 'v'; if((param.get() || defaultOrientation) === contFull.orientation) { continue; } } else if(ai === 'orientationaxes') { // orientationaxes has no value, // it flips everything and the axes cont.orientation = {v: 'h', h: 'v'}[contFull.orientation]; } helpers.swapXYData(cont); flags.calc = flags.clearAxisTypes = true; } else if(Plots.dataArrayContainers.indexOf(param.parts[0]) !== -1) { // TODO: use manageArrays.applyContainerArrayChanges here too helpers.manageArrayContainers(param, newVal, undoit); flags.calc = true; } else { if(valObject) { // must redo calcdata when restyling array values of arrayOk attributes // ... but no need to this for regl-based traces if(valObject.arrayOk && !Registry.traceIs(contFull, 'regl') && (Lib.isArrayOrTypedArray(newVal) || Lib.isArrayOrTypedArray(oldVal)) ) { flags.calc = true; } else editTypes.update(flags, valObject); } else { /* * if we couldn't find valObject, assume a full recalc. * This can happen if you're changing type and making * some other edits too, so the modules we're * looking at don't have these attributes in them. */ flags.calc = true; } // all the other ones, just modify that one attribute param.set(newVal); } } // swap the data attributes of the relevant x and y axes? if(['swapxyaxes', 'orientationaxes'].indexOf(ai) !== -1) { Axes.swap(gd, traces); } // swap hovermode if set to "compare x/y data" if(ai === 'orientationaxes') { var hovermode = nestedProperty(gd.layout, 'hovermode'); var h = hovermode.get(); if(h === 'x') { hovermode.set('y'); } else if(h === 'y') { hovermode.set('x'); } else if(h === 'x unified') { hovermode.set('y unified'); } else if(h === 'y unified') { hovermode.set('x unified'); } } // Major enough changes deserve autoscale and // non-reversed axes so people don't get confused // // Note: autobin (or its new analog bin clearing) is not included here // since we're not pushing bins back to gd.data, so if we have bin // info it was explicitly provided by the user. 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'); } } doextra(axlist.map(autorangeAttr), true, 0); doextra(axlist.map(rangeAttr), [0, 1], 0); } } if(flags.calc || flags.plot) { flags.fullReplot = true; } return { flags: flags, undoit: undoit, redoit: redoit, traces: traces, eventData: Lib.extendDeepNoArrays([], [eventData, traces]) }; } /** * Converts deprecated attribute keys to * the current API to ensure backwards compatibility. * * This is needed for the update mechanism to determine which * subroutines to run based on the actual attribute * definitions (that don't include the deprecated ones). * * E.g. Maps {'xaxis.title': 'A chart'} to {'xaxis.title.text': 'A chart'} * and {titlefont: {...}} to {'title.font': {...}}. * * @param aobj */ function cleanDeprecatedAttributeKeys(aobj) { var oldAxisTitleRegex = Lib.counterRegex('axis', '\.title', false, false); var colorbarRegex = /colorbar\.title$/; var keys = Object.keys(aobj); var i, key, value; for(i = 0; i < keys.length; i++) { key = keys[i]; value = aobj[key]; if((key === 'title' || oldAxisTitleRegex.test(key) || colorbarRegex.test(key)) && (typeof value === 'string' || typeof value === 'number')) { replace(key, key.replace('title', 'title.text')); } else if(key.indexOf('titlefont') > -1 && key.indexOf('grouptitlefont') === -1) { replace(key, key.replace('titlefont', 'title.font')); } else if(key.indexOf('titleposition') > -1) { replace(key, key.replace('titleposition', 'title.position')); } else if(key.indexOf('titleside') > -1) { replace(key, key.replace('titleside', 'title.side')); } else if(key.indexOf('titleoffset') > -1) { replace(key, key.replace('titleoffset', 'title.offset')); } } function replace(oldAttrStr, newAttrStr) { aobj[newAttrStr] = aobj[oldAttrStr]; delete aobj[oldAttrStr]; } } /** * 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 */ function relayout(gd, astr, val) { gd = Lib.getGraphDiv(gd); helpers.clearPromiseQueue(gd); var aobj = {}; if(typeof astr === 'string') { aobj[astr] = val; } else if(Lib.isPlainObject(astr)) { aobj = Lib.extendFlat({}, astr); } else { Lib.warn('Relayout fail.', astr, val); return Promise.reject(); } if(Object.keys(aobj).length) gd.changed = true; var specs = _relayout(gd, aobj); var flags = specs.flags; // clear calcdata if required if(flags.calc) gd.calcdata = undefined; // fill in redraw sequence // even if we don't have anything left in aobj, // something may have happened within relayout that we // need to wait for var seq = [Plots.previousPromises]; if(flags.layoutReplot) { seq.push(subroutines.layoutReplot); } else if(Object.keys(aobj).length) { axRangeSupplyDefaultsByPass(gd, flags, specs) || Plots.supplyDefaults(gd); if(flags.legend) seq.push(subroutines.doLegend); if(flags.layoutstyle) seq.push(subroutines.layoutStyles); if(flags.axrange) addAxRangeSequence(seq, specs.rangesAltered); if(flags.ticks) seq.push(subroutines.doTicksRelayout); if(flags.modebar) seq.push(subroutines.doModeBar); if(flags.camera) seq.push(subroutines.doCamera); if(flags.colorbars) seq.push(subroutines.doColorBars); seq.push(emitAfterPlot); } seq.push( Plots.rehover, Plots.redrag, Plots.reselect ); 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() { gd.emit('plotly_relayout', specs.eventData); return gd; }); } // Optimization mostly for large splom traces where // Plots.supplyDefaults can take > 100ms function axRangeSupplyDefaultsByPass(gd, flags, specs) { var fullLayout = gd._fullLayout; if(!flags.axrange) return false; for(var k in flags) { if(k !== 'axrange' && flags[k]) return false; } var axIn, axOut; var coerce = function(attr, dflt) { return Lib.coerce(axIn, axOut, cartesianLayoutAttributes, attr, dflt); }; var options = {}; // passing empty options for now! for(var axId in specs.rangesAltered) { var axName = Axes.id2name(axId); axIn = gd.layout[axName]; axOut = fullLayout[axName]; handleRangeDefaults(axIn, axOut, coerce, options); if(axOut._matchGroup) { for(var axId2 in axOut._matchGroup) { if(axId2 !== axId) { var ax2 = fullLayout[Axes.id2name(axId2)]; ax2.autorange = axOut.autorange; ax2.range = axOut.range.slice(); ax2._input.range = axOut.range.slice(); } } } } return true; } function addAxRangeSequence(seq, rangesAltered) { // N.B. leave as sequence of subroutines (for now) instead of // subroutine of its own so that finalDraw always gets // executed after drawData var drawAxes = rangesAltered ? function(gd) { var axIds = []; var skipTitle = true; for(var id in rangesAltered) { var ax = Axes.getFromId(gd, id); axIds.push(id); if((ax.ticklabelposition || '').indexOf('inside') !== -1) { if(ax._anchorAxis) { axIds.push(ax._anchorAxis._id); } } if(ax._matchGroup) { for(var id2 in ax._matchGroup) { if(!rangesAltered[id2]) { axIds.push(id2); } } } } return Axes.draw(gd, axIds, {skipTitle: skipTitle}); } : function(gd) { return Axes.draw(gd, 'redraw'); }; seq.push( clearOutline, subroutines.doAutoRangeAndConstraints, drawAxes, subroutines.drawData, subroutines.finalDraw ); } var AX_RANGE_RE = /^[xyz]axis[0-9]*\.range(\[[0|1]\])?$/; var AX_AUTORANGE_RE = /^[xyz]axis[0-9]*\.autorange$/; var AX_DOMAIN_RE = /^[xyz]axis[0-9]*\.domain(\[[0|1]\])?$/; function _relayout(gd, aobj) { var layout = gd.layout; var fullLayout = gd._fullLayout; var guiEditFlag = fullLayout._guiEditing; var layoutNP = makeNP(fullLayout._preGUI, guiEditFlag); var keys = Object.keys(aobj); var axes = Axes.list(gd); var eventData = Lib.extendDeepAll({}, aobj); var arrayEdits = {}; var arrayStr, i, j; cleanDeprecatedAttributeKeys(aobj); keys = Object.keys(aobj); // 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(j = 0; j < axes.length; j++) { var scene = axes[j]._id.substr(1); var axisAttr = (scene.indexOf('scene') !== -1) ? (scene + '.') : ''; var 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 = editTypes.layoutFlags(); // copies of the change (and previous values of anything affected) // for the undo / redo queue var redoit = {}; var undoit = {}; // 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; } // if we have another value for this attribute (explicitly or // via a parent) do not override with this auto-generated extra if(attr in aobj || helpers.hasParent(aobj, attr)) return; var p = layoutNP(layout, attr); if(!(attr in undoit)) { undoit[attr] = undefinedToNull(p.get()); } if(val !== undefined) p.set(val); } // for constraint enforcement: keep track of all axes (as {id: name}) // we're editing the (auto)range of, so we can tell the others constrained // to scale with them that it's OK for them to shrink var rangesAltered = {}; var ax; function recordAlteredAxis(pleafPlus) { var axId = Axes.name2id(pleafPlus.split('.')[0]); rangesAltered[axId] = 1; return axId; } // alter gd.layout for(var ai in aobj) { if(helpers.hasParent(aobj, ai)) { throw new Error('cannot set ' + ai + ' and a parent attribute simultaneously'); } var p = layoutNP(layout, ai); var vi = aobj[ai]; var plen = p.parts.length; // p.parts may end with an index integer if the property is an array var pend = plen - 1; while(pend > 0 && typeof p.parts[pend] !== 'string') pend--; // last property in chain (leaf node) var pleaf = p.parts[pend]; // leaf plus immediate parent var pleafPlus = p.parts[pend - 1] + '.' + pleaf; // trunk nodes (everything except the leaf) var ptrunk = p.parts.slice(0, pend).join('.'); var parentIn = nestedProperty(gd.layout, ptrunk).get(); var parentFull = nestedProperty(fullLayout, ptrunk).get(); var vOld = p.get(); 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 : undefinedToNull(vOld); var valObject = PlotSchema.getLayoutValObject(fullLayout, p.parts); if(valObject && valObject.impliedEdits && vi !== null) { for(var impliedKey in valObject.impliedEdits) { doextra(Lib.relativeAttr(ai, impliedKey), valObject.impliedEdits[impliedKey]); } } // Setting width or height to null must reset the graph's width / height // back to its initial value as computed during the first pass in Plots.plotAutoSize. // // To do so, we must manually set them back here using the _initialAutoSize cache. // can't use impliedEdits for this because behavior depends on vi if(['width', 'height'].indexOf(ai) !== -1) { if(vi) { doextra('autosize', null); // currently we don't support autosize one dim only - so // explicitly set the other one. Note that doextra will // ignore this if the same relayout call also provides oppositeAttr var oppositeAttr = ai === 'height' ? 'width' : 'height'; doextra(oppositeAttr, fullLayout[oppositeAttr]); } else { fullLayout[ai] = gd._initialAutoSize[ai]; } } else if(ai === 'autosize') { // depends on vi here too, so again can't use impliedEdits doextra('width', vi ? null : fullLayout.width); doextra('height', vi ? null : fullLayout.height); } else if(pleafPlus.match(AX_RANGE_RE)) { // check autorange vs range recordAlteredAxis(pleafPlus); nestedProperty(fullLayout, ptrunk + '._inputRange').set(null); } else if(pleafPlus.match(AX_AUTORANGE_RE)) { recordAlteredAxis(pleafPlus); nestedProperty(fullLayout, ptrunk + '._inputRange').set(null); var axFull = nestedProperty(fullLayout, ptrunk).get(); if(axFull._inputDomain) { // if we're autoranging and this axis has a constrained domain, // reset it so we don't get locked into a shrunken size axFull._input.domain = axFull._inputDomain.slice(); } } else if(pleafPlus.match(AX_DOMAIN_RE)) { nestedProperty(fullLayout, ptrunk + '._inputDomain').set(null); } // toggling axis type between log and linear: we need to convert // positions for components that are still using linearized values, // not data values like newer components. // previously we did this for log <-> not-log, but now only do it // for log <-> linear if(pleaf === 'type') { ax = parentIn; var toLog = parentFull.type === 'linear' && vi === 'log'; var fromLog = parentFull.type === 'log' && vi === 'linear'; if(toLog || fromLog) { if(!ax || !ax.range) { // 2D never gets here, but 3D does // I don't think this is needed, but left here in case there // are edge cases I'm not thinking of. doextra(ptrunk + '.autorange', true); } else if(!parentFull.autorange) { // toggling log without autorange: need to also recalculate ranges // because log axes use linearized values for range endpoints var r0 = ax.range[0]; var r1 = ax.range[1]; if(toLog) { // 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(toLog) { // 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]; } // clear polar view initial stash for radial range so that // value get recomputed in correct units if(Array.isArray(fullLayout._subplots.polar) && fullLayout._subplots.polar.length && fullLayout[p.parts[0]] && p.parts[1] === 'radialaxis' ) { delete fullLayout[p.parts[0]]._subplot.viewInitial['radialaxis.range']; } // Annotations and images also need to convert to/from linearized coords // Shapes do not need this :) Registry.getComponentMethod('annotations', 'convertCoords')(gd, parentFull, vi, doextra); Registry.getComponentMethod('images', 'convertCoords')(gd, parentFull, vi, doextra); } else { // any other type changes: the range from the previous type // will not make sense, so autorange it. doextra(ptrunk + '.autorange', true); doextra(ptrunk + '.range', null); } nestedProperty(fullLayout, ptrunk + '._inputRange').set(null); } else if(pleaf.match(AX_NAME_PATTERN)) { var fullProp = nestedProperty(fullLayout, ai).get(); var newType = (vi || {}).type; // This can potentially cause strange behavior if the autotype is not // numeric (linear, because we don't auto-log) but the previous type // was log. That's a very strange edge case though if(!newType || newType === '-') newType = 'linear'; Registry.getComponentMethod('annotations', 'convertCoords')(gd, fullProp, newType, doextra); Registry.getComponentMethod('images', 'convertCoords')(gd, fullProp, newType, doextra); } // alter gd.layout // collect array component edits for execution all together // so we can ensure consistent behavior adding/removing items // and order-independence for add/remove/edit all together in // one relayout call var containerArrayMatch = manageArrays.containerArrayMatch(ai); if(containerArrayMatch) { arrayStr = containerArrayMatch.array; i = containerArrayMatch.index; var propStr = containerArrayMatch.property; var updateValObject = valObject || {editType: 'calc'}; if(i !== '' && propStr === '') { // special handling of undoit if we're adding or removing an element // ie 'annotations[2]' which can be {...} (add) or null, // does not work when replacing the entire array if(manageArrays.isAddVal(vi)) { undoit[ai] = null; } else if(manageArrays.isRemoveVal(vi)) { undoit[ai] = (nestedProperty(layout, arrayStr).get() || [])[i]; } else { Lib.warn('unrecognized full object value', aobj); } } editTypes.update(flags, updateValObject); // prepare the edits object we'll send to applyContainerArrayChanges if(!arrayEdits[arrayStr]) arrayEdits[arrayStr] = {}; var objEdits = arrayEdits[arrayStr][i]; if(!objEdits) objEdits = arrayEdits[arrayStr][i] = {}; objEdits[propStr] = vi; delete aobj[ai]; } else if(pleaf === 'reverse') { // handle axis reversal explicitly, as there's no 'reverse' attribute if(parentIn.range) parentIn.range.reverse(); else { doextra(ptrunk + '.autorange', true); parentIn.range = [1, 0]; } if(parentFull.autorange) flags.calc = true; else flags.plot = true; } else { if(ai === 'dragmode' && ((vi === false && vOld !== false) || (vi !== false && vOld === false))) { flags.plot = true; } else if((fullLayout._has('scatter-like') && fullLayout._has('regl')) && (ai === 'dragmode' && (vi === 'lasso' || vi === 'select') && !(vOld === 'lasso' || vOld === 'select')) ) { flags.plot = true; } else if(fullLayout._has('gl2d')) { flags.plot = true; } else if(valObject) editTypes.update(flags, valObject); else flags.calc = true; p.set(vi); } } // now we've collected component edits - execute them all together for(arrayStr in arrayEdits) { var finished = manageArrays.applyContainerArrayChanges(gd, layoutNP(layout, arrayStr), arrayEdits[arrayStr], flags, layoutNP); if(!finished) flags.plot = true; } // figure out if we need to recalculate axis constraints for(var axId in rangesAltered) { ax = Axes.getFromId(gd, axId); var group = ax && ax._constraintGroup; if(group) { // Always recalc if we're changing constrained ranges. // Otherwise it's possible to violate the constraints by // specifying arbitrary ranges for all axes in the group. // this way some ranges may expand beyond what's specified, // as they do at first draw, to satisfy the constraints. flags.calc = true; for(var groupAxId in group) { if(!rangesAltered[groupAxId]) { Axes.getFromId(gd, groupAxId)._constraintShrinkable = true; } } } } // If the autosize changed or height or width was explicitly specified, // this triggers a redraw // TODO: do we really need special aobj.height/width handling here? // couldn't editType do this? if(updateAutosize(gd) || aobj.height || aobj.width) flags.plot = true; // update shape legends var shapes = fullLayout.shapes; for(i = 0; i < shapes.length; i++) { if(shapes[i].showlegend) { flags.calc = true; break; } } if(flags.plot || flags.calc) { flags.layoutReplot = true; } // now all attribute mods are done, as are // redo and undo so we can save them return { flags: flags, rangesAltered: rangesAltered, undoit: undoit, redoit: redoit, eventData: eventData }; } /* * updateAutosize: we made a change, does it change the autosize result? * puts the new size into fullLayout * returns true if either height or width changed */ function updateAutosize(gd) { var fullLayout = gd._fullLayout; var oldWidth = fullLayout.width; var oldHeight = fullLayout.height; // calculate autosizing if(gd.layout.autosize) Plots.plotAutoSize(gd, gd.layout, fullLayout); return (fullLayout.width !== oldWidth) || (fullLayout.height !== oldHeight); } /** * 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) * */ function update(gd, traceUpdate, layoutUpdate, _traces) { gd = Lib.getGraphDiv(gd); helpers.clearPromiseQueue(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 traces = helpers.coerceTraceIndices(gd, _traces); var restyleSpecs = _restyle(gd, Lib.extendFlat({}, traceUpdate), traces); var restyleFlags = restyleSpecs.flags; var relayoutSpecs = _relayout(gd, Lib.extendFlat({}, layoutUpdate)); var relayoutFlags = relayoutSpecs.flags; // clear calcdata and/or axis types if required if(restyleFlags.calc || relayoutFlags.calc) gd.calcdata = undefined; if(restyleFlags.clearAxisTypes) helpers.clearAxisTypes(gd, traces, layoutUpdate); // fill in redraw sequence var seq = []; if(relayoutFlags.layoutReplot) { // N.B. works fine when both // relayoutFlags.layoutReplot and restyleFlags.fullReplot are true seq.push(subroutines.layoutReplot); } else if(restyleFlags.fullReplot) { seq.push(exports._doPlot); } else { seq.push(Plots.previousPromises); axRangeSupplyDefaultsByPass(gd, relayoutFlags, relayoutSpecs) || Plots.supplyDefaults(gd); if(restyleFlags.style) seq.push(subroutines.doTraceStyle); if(restyleFlags.colorbars || relayoutFlags.colorbars) seq.push(subroutines.doColorBars); if(relayoutFlags.legend) seq.push(subroutines.doLegend); if(relayoutFlags.layoutstyle) seq.push(subroutines.layoutStyles); if(relayoutFlags.axrange) addAxRangeSequence(seq, relayoutSpecs.rangesAltered); if(relayoutFlags.ticks) seq.push(subroutines.doTicksRelayout); if(relayoutFlags.modebar) seq.push(subroutines.doModeBar); if(relayoutFlags.camera) seq.push(subroutines.doCamera); seq.push(emitAfterPlot); } seq.push( Plots.rehover, Plots.redrag, Plots.reselect ); 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() { gd.emit('plotly_update', { data: restyleSpecs.eventData, layout: relayoutSpecs.eventData }); return gd; }); } /* * internal-use-only restyle/relayout/update variants that record the initial * values in (fullLayout|fullTrace)._preGUI so changes can be persisted across * Plotly.react data updates, dependent on uirevision attributes */ function guiEdit(func) { return function wrappedEdit(gd) { gd._fullLayout._guiEditing = true; var p = func.apply(null, arguments); gd._fullLayout._guiEditing = false; return p; }; } // For connecting edited layout attributes to uirevision attrs // If no `attr` we use `match[1] + '.uirevision'` // Ordered by most common edits first, to minimize our search time var layoutUIControlPatterns = [ {pattern: /^hiddenlabels/, attr: 'legend.uirevision'}, {pattern: /^((x|y)axis\d*)\.((auto)?range|title\.text)/}, // showspikes and modes include those nested inside scenes {pattern: /axis\d*\.showspikes$/, attr: 'modebar.uirevision'}, {pattern: /(hover|drag)mode$/, attr: 'modebar.uirevision'}, {pattern: /^(scene\d*)\.camera/}, {pattern: /^(geo\d*)\.(projection|center|fitbounds)/}, {pattern: /^(ternary\d*\.[abc]axis)\.(min|title\.text)$/}, {pattern: /^(polar\d*\.radialaxis)\.((auto)?range|angle|title\.text)/}, {pattern: /^(polar\d*\.angularaxis)\.rotation/}, {pattern: /^(mapbox\d*)\.(center|zoom|bearing|pitch)/}, {pattern: /^(map\d*)\.(center|zoom|bearing|pitch)/}, {pattern: /^legend\.(x|y)$/, attr: 'editrevision'}, {pattern: /^(shapes|annotations)/, attr: 'editrevision'}, {pattern: /^title\.text$/, attr: 'editrevision'} ]; // same for trace attributes: if `attr` is given it's in layout, // or with no `attr` we use `trace.uirevision` var traceUIControlPatterns = [ {pattern: /^selectedpoints$/, attr: 'selectionrevision'}, // "visible" includes trace.transforms[i].styles[j].value.visible {pattern: /(^|value\.)visible$/, attr: 'legend.uirevision'}, {pattern: /^dimensions\[\d+\]\.constraintrange/}, {pattern: /^node\.(x|y|groups)/}, // for Sankey nodes {pattern: /^level$/}, // for Sunburst, Treemap and Icicle traces // below this you must be in editable: true mode // TODO: I still put name and title with `trace.uirevision` // reasonable or should these be `editrevision`? // Also applies to axis titles up in the layout section // "name" also includes transform.styles {pattern: /(^|value\.)name$/}, // including nested colorbar attributes (ie marker.colorbar) {pattern: /colorbar\.title\.text$/}, {pattern: /colorbar\.(x|y)$/, attr: 'editrevision'} ]; function findUIPattern(key, patternSpecs) { for(var i = 0; i < patternSpecs.length; i++) { var spec = patternSpecs[i]; var match = key.match(spec.pattern); if(match) { var head = match[1] || ''; return {head: head, tail: key.substr(head.length + 1), attr: spec.attr}; } } } // We're finding the new uirevision before supplyDefaults, so do the // inheritance manually. Note that only `undefined` inherits - other // falsy values are returned. function getNewRev(revAttr, container) { var newRev = nestedProperty(container, revAttr).get(); if(newRev !== undefined) return newRev; var parts = revAttr.split('.'); parts.pop(); while(parts.length > 1) { parts.pop(); newRev = nestedProperty(container, parts.join('.') + '.uirevision').get(); if(newRev !== undefined) return newRev; } return container.uirevision; } function getFullTraceIndexFromUid(uid, fullData) { for(var i = 0; i < fullData.length; i++) { if(fullData[i]._fullInput.uid === uid) return i; } return -1; } function getTraceIndexFromUid(uid, data, tracei) { for(var i = 0; i < data.length; i++) { if(data[i].uid === uid) return i; } // fall back on trace order, but only if user didn't provide a uid for that trace return (!data[tracei] || data[tracei].uid) ? -1 : tracei; } function valsMatch(v1, v2) { var v1IsObj = Lib.isPlainObject(v1); var v1IsArray = Array.isArray(v1); if(v1IsObj || v1IsArray) { return ( (v1IsObj && Lib.isPlainObject(v2)) || (v1IsArray && Array.isArray(v2)) ) && JSON.stringify(v1) === JSON.stringify(v2); } return v1 === v2; } function applyUIRevisions(data, layout, oldFullData, oldFullLayout) { var layoutPreGUI = oldFullLayout._preGUI; var key, revAttr, oldRev, newRev, match, preGUIVal, newNP, newVal, head, tail; var bothInheritAutorange = []; var newAutorangeIn = {}; var newRangeAccepted = {}; for(key in layoutPreGUI) { match = findUIPattern(key, layoutUIControlPatterns); if(match) { head = match.head; tail = match.tail; revAttr = match.attr || (head + '.uirevision'); oldRev = nestedProperty(oldFullLayout, revAttr).get(); newRev = oldRev && getNewRev(revAttr, layout); if(newRev && (newRev === oldRev)) { preGUIVal = layoutPreGUI[key]; if(preGUIVal === null) preGUIVal = undefined; newNP = nestedProperty(layout, key); newVal = newNP.get(); if(valsMatch(newVal, preGUIVal)) { if(newVal === undefined && tail === 'autorange') { bothInheritAutorange.push(head); } newNP.set(undefinedToNull(nestedProperty(oldFullLayout, key).get())); continue; } else if(tail === 'autorange' || tail.substr(0, 6) === 'range[') { // Special case for (auto)range since we push it back into the layout // so all null should be treated equivalently to autorange: true with any range var pre0 = layoutPreGUI[head + '.range[0]']; var pre1 = layoutPreGUI[head + '.range[1]']; var preAuto = layoutPreGUI[head + '.autorange']; if(preAuto || (preAuto === null && pre0 === null && pre1 === null)) { // Only read the input layout once and stash the result, // so we get it before we start modifying it if(!(head in newAutorangeIn)) { var newContainer = nestedProperty(layout, head).get(); newAutorangeIn[head] = newContainer && ( newContainer.autorange || (newContainer.autorange !== false && ( !newContainer.range || newContainer.range.length !== 2) ) ); } if(newAutorangeIn[head]) { newNP.set(undefinedToNull(nestedProperty(oldFullLayout, key).get())); continue; } } } } } else { Lib.warn('unrecognized GUI edit: ' + key); } // if we got this far, the new value was accepted as the new starting // point (either because it changed or revision changed) // so remove it from _preGUI for next time. delete layoutPreGUI[key]; if(match && match.tail.substr(0, 6) === 'range[') { newRangeAccepted[match.head] = 1; } } // More special logic for `autorange`, since it interacts with `range`: // If the new figure's matching `range` was kept, and `autorange` // wasn't supplied explicitly in either the original or the new figure, // we shouldn't alter that - but we may just have done that, so fix it. for(var i = 0; i < bothInheritAutorange.length; i++) { var axAttr = bothInheritAutorange[i]; if(newRangeAccepted[axAttr]) { var newAx = nestedProperty(layout, axAttr).get(); if(newAx) delete newAx.autorange; } } // Now traces - try to match them up by uid (in case we added/deleted in // the middle), then fall back on index. var allTracePreGUI = oldFullLayout._tracePreGUI; for(var uid in allTracePreGUI) { var tracePreGUI = allTracePreGUI[uid]; var newTrace = null; var fullInput; for(key in tracePreGUI) { // wait until we know we have preGUI values to look for traces // but if we don't find both, stop looking at this uid if(!newTrace) { var fulli = getFullTraceIndexFromUid(uid, oldFullData); if(fulli < 0) { // Somehow we didn't even have this trace in oldFullData... // I guess this could happen with `deleteTraces` or something delete allTracePreGUI[uid]; break; } var fullTrace = oldFullData[fulli]; fullInput = fullTrace._fullInput; var newTracei = getTraceIndexFromUid(uid, data, fullInput.index); if(newTracei < 0) { // No match in new data delete allTracePreGUI[uid]; break; } newTrace = data[newTracei]; } match = findUIPattern(key, traceUIControlPatterns); if(match) { if(match.attr) { oldRev = nestedProperty(oldFullLayout, match.attr).get(); newRev = oldRev && getNewRev(match.attr, layout); } else { oldRev = fullInput.uirevision; // inheritance for trace.uirevision is simple, just layout.uirevision newRev = newTrace.uirevision; if(newRev === undefined) newRev = layout.uirevision; } if(newRev && newRev === oldRev) { preGUIVal = tracePreGUI[key]; if(preGUIVal === null) preGUIVal = undefined; newNP = nestedProperty(newTrace, key); newVal = newNP.get(); if(valsMatch(newVal, preGUIVal)) { newNP.set(undefinedToNull(nestedProperty(fullInput, key).get())); continue; } } } else { Lib.warn('unrecognized GUI edit: ' + key + ' in trace uid ' + uid); } delete tracePreGUI[key]; } } } /** * Plotly.react: * A plot/update method that takes the full plot state (same API as plot/newPlot) * and diffs to determine the minimal update pathway * * @param {string id or DOM element} gd * the id or DOM element of the graph container div * @param {array of objects} data * array of traces, containing the data and display information for each trace * @param {object} layout * object describing the overall display of the plot, * all the stuff that doesn't pertain to any individual trace * @param {object} config * configuration options (see ./plot_config.js for more info) * * OR * * @param {string id or DOM element} gd * the id or DOM element of the graph container div * @param {object} figure * object containing `data`, `layout`, `config`, and `frames` members * */ function react(gd, data, layout, config) { var frames, plotDone; function addFrames() { return exports.addFrames(gd, frames); } gd = Lib.getGraphDiv(gd); helpers.clearPromiseQueue(gd); var oldFullData = gd._fullData; var oldFullLayout = gd._fullLayout; // you can use this as the initial draw as well as to update if(!Lib.isPlotDiv(gd) || !oldFullData || !oldFullLayout) { plotDone = exports.newPlot(gd, data, layout, config); } else { if(Lib.isPlainObject(data)) { var obj = data; data = obj.data; layout = obj.layout; config = obj.config; frames = obj.frames; } var configChanged = false; // assume that if there's a config at all, we're reacting to it too, // and completely replace the previous config if(config) { var oldConfig = Lib.extendDeep({}, gd._context); gd._context = undefined; setPlotContext(gd, config); configChanged = diffConfig(oldConfig, gd._context); } gd.data = data || []; helpers.cleanData(gd.data); gd.layout = layout || {}; helpers.cleanLayout(gd.layout); applyUIRevisions(gd.data, gd.layout, oldFullData, oldFullLayout); // "true" skips updating calcdata and remapping arrays from calcTransforms, // which supplyDefaults usually does at the end, but we may need to NOT do // if the diff (which we haven't determined yet) says we'll recalc Plots.supplyDefaults(gd, {skipUpdateCalc: true}); var newFullData = gd._fullData; var newFullLayout = gd._fullLayout; var immutable = newFullLayout.datarevision === undefined; var transition = newFullLayout.transition; var relayoutFlags = diffLayout(gd, oldFullLayout, newFullLayout, immutable, transition); var newDataRevision = relayoutFlags.newDataRevision; var restyleFlags = diffData(gd, oldFullData, newFullData, immutable, transition, newDataRevision); // TODO: how to translate this part of relayout to Plotly.react? // // Setting width or height to null must reset the graph's width / height // // back to its initial value as computed during the first pass in Plots.plotAutoSize. // // // // To do so, we must manually set them back here using the _initialAutoSize cache. // if(['width', 'height'].indexOf(ai) !== -1 && vi === null) { // fullLayout[ai] = gd._initialAutoSize[ai]; // } if(updateAutosize(gd)) relayoutFlags.layoutReplot = true; // clear calcdata and empty categories if required if(restyleFlags.calc || relayoutFlags.calc) { gd.calcdata = undefined; var allNames = Object.getOwnPropertyNames(newFullLayout); for(var q = 0; q < allNames.length; q++) { var name = allNames[q]; var start = name.substring(0, 5); if(start === 'xaxis' || start === 'yaxis') { var emptyCategories = newFullLayout[name]._emptyCategories; if(emptyCategories) emptyCategories(); } } // otherwise do the calcdata updates and calcTransform array remaps that we skipped earlier } else { Plots.supplyDefaultsUpdateCalc(gd.calcdata, newFullData); } // Note: what restyle/relayout use impliedEdits and clearAxisTypes for // must be handled by the user when using Plotly.react. // fill in redraw sequence var seq = []; if(frames) { gd._transitionData = {}; Plots.createTransitionData(gd); seq.push(addFrames); } // Transition pathway, // only used when 'transition' is set by user and // when at least one animatable attribute has changed, // N.B. config changed aren't animatable if(newFullLayout.transition && !configChanged && (restyleFlags.anim || relayoutFlags.anim)) { if(relayoutFlags.ticks) seq.push(subroutines.doTicksRelayout); Plots.doCalcdata(gd); subroutines.doAutoRangeAndConstraints(gd); seq.push(function() { return Plots.transitionFromReact(gd, restyleFlags, relayoutFlags, oldFullLayout); }); } else if(restyleFlags.fullReplot || relayoutFlags.layoutReplot || configChanged) { gd._fullLayout._skipDefaults = true; seq.push(exports._doPlot); } else { for(var componentType in relayoutFlags.arrays) { var indices = relayoutFlags.arrays[componentType]; if(indices.length) { var drawOne = Registry.getComponentMethod(componentType, 'drawOne'); if(drawOne !== Lib.noop) { for(var i = 0; i < indices.length; i++) { drawOne(gd, indices[i]); } } else { var draw = Registry.getComponentMethod(componentType, 'draw'); if(draw === Lib.noop) { throw new Error('cannot draw components: ' + componentType); } draw(gd); } } } seq.push(Plots.previousPromises); if(restyleFlags.style) seq.push(subroutines.doTraceStyle); if(restyleFlags.colorbars || relayoutFlags.colorbars) seq.push(subroutines.doColorBars); if(relayoutFlags.legend) seq.push(subroutines.doLegend); if(relayoutFlags.layoutstyle) seq.push(subroutines.layoutStyles); if(relayoutFlags.axrange) addAxRangeSequence(seq); if(relayoutFlags.ticks) seq.push(subroutines.doTicksRelayout); if(relayoutFlags.modebar) seq.push(subroutines.doModeBar); if(relayoutFlags.camera) seq.push(subroutines.doCamera); seq.push(emitAfterPlot); } seq.push( Plots.rehover, Plots.redrag, Plots.reselect ); plotDone = Lib.syncOrAsync(seq, gd); if(!plotDone || !plotDone.then) plotDone = Promise.resolve(gd); } return plotDone.then(function() { gd.emit('plotly_react', { data: data, layout: layout }); return gd; }); } function diffData(gd, oldFullData, newFullData, immutable, transition, newDataRevision) { var sameTraceLength = oldFullData.length === newFullData.length; if(!transition && !sameTraceLength) { return { fullReplot: true, calc: true }; } var flags = editTypes.traceFlags(); flags.arrays = {}; flags.nChanges = 0; flags.nChangesAnim = 0; var i, trace; function getTraceValObject(parts) { var out = PlotSchema.getTraceValObject(trace, parts); if(!trace._module.animatable && out.anim) { out.anim = false; } return out; } var diffOpts = { getValObject: getTraceValObject, flags: flags, immutable: immutable, transition: transition, newDataRevision: newDataRevision, gd: gd }; var seenUIDs = {}; for(i = 0; i < oldFullData.length; i++) { if(newFullData[i]) { trace = newFullData[i]._fullInput; if(Plots.hasMakesDataTransform(trace)) trace = newFullData[i]; if(seenUIDs[trace.uid]) continue; seenUIDs[trace.uid] = 1; getDiffFlags(oldFullData[i]._fullInput, trace, [], diffOpts); } } if(flags.calc || flags.plot) { flags.fullReplot = true; } if(transition && flags.nChanges && flags.nChangesAnim) { flags.anim = (flags.nChanges === flags.nChangesAnim) && sameTraceLength ? 'all' : 'some'; } return flags; } function diffLayout(gd, oldFullLayout, newFullLayout, immutable, transition) { var flags = editTypes.layoutFlags(); flags.arrays = {}; flags.rangesAltered = {}; flags.nChanges = 0; flags.nChangesAnim = 0; function getLayoutValObject(parts) { return PlotSchema.getLayoutValObject(newFullLayout, parts); } var diffOpts = { getValObject: getLayoutValObject, flags: flags, immutable: immutable, transition: transition, gd: gd }; getDiffFlags(oldFullLayout, newFullLayout, [], diffOpts); if(flags.plot || flags.calc) { flags.layoutReplot = true; } if(transition && flags.nChanges && flags.nChangesAnim) { flags.anim = flags.nChanges === flags.nChangesAnim ? 'all' : 'some'; } return flags; } function getDiffFlags(oldContainer, newContainer, outerparts, opts) { var valObject, key, astr; var getValObject = opts.getValObject; var flags = opts.flags; var immutable = opts.immutable; var inArray = opts.inArray; var arrayIndex = opts.arrayIndex; function changed() { var editType = valObject.editType; if(inArray && editType.indexOf('arraydraw') !== -1) { Lib.pushUnique(flags.arrays[inArray], arrayIndex); return; } editTypes.update(flags, valObject); if(editType !== 'none') { flags.nChanges++; } // track animatable changes if(opts.transition && valObject.anim) { flags.nChangesAnim++; } // track cartesian axes with altered ranges if(AX_RANGE_RE.test(astr) || AX_AUTORANGE_RE.test(astr)) { flags.rangesAltered[outerparts[0]] = 1; } // clear _inputDomain on cartesian axes with altered domains if(AX_DOMAIN_RE.test(astr)) { nestedProperty(newContainer, '_inputDomain').set(null); } // track datarevision changes if(key === 'datarevision') { flags.newDataRevision = 1; } } function valObjectCanBeDataArray(valObject) { return valObject.valType === 'data_array' || valObject.arrayOk; } for(key in oldContainer) { // short-circuit based on previous calls or previous keys that already maximized the pathway if(flags.calc && !opts.transition) return; var oldVal = oldContainer[key]; var newVal = newContainer[key]; var parts = outerparts.concat(key); astr = parts.join('.'); if(key.charAt(0) === '_' || typeof oldVal === 'function' || oldVal === newVal) continue; // FIXME: ax.tick0 and dtick get filled in during plotting (except for geo subplots), // and unlike other auto values they don't make it back into the input, // so newContainer won't have them. if((key === 'tick0' || key === 'dtick') && outerparts[0] !== 'geo') { var tickMode = newContainer.tickmode; if(tickMode === 'auto' || tickMode === 'array' || !tickMode) continue; } // FIXME: Similarly for axis ranges for 3D // contourcarpet doesn't HAVE zmin/zmax, they're just auto-added. It needs them. if(key === 'range' && newContainer.autorange) continue; if((key === 'zmin' || key === 'zmax') && newContainer.type === 'contourcarpet') continue; valObject = getValObject(parts); // in case type changed, we may not even *have* a valObject. if(!valObject) continue; if(valObject._compareAsJSON && JSON.stringify(oldVal) === JSON.stringify(newVal)) continue; var valType = valObject.valType; var i; var canBeDataArray = valObjectCanBeDataArray(valObject); var wasArray = Array.isArray(oldVal); var nowArray = Array.isArray(newVal); // hack for traces that modify the data in supplyDefaults, like // converting 1D to 2D arrays, which will always create new objects if(wasArray && nowArray) { var inputKey = '_input_' + key; var oldValIn = oldContainer[inputKey]; var newValIn = newContainer[inputKey]; if(Array.isArray(oldValIn) && oldValIn === newValIn) continue; } if(newVal === undefined) { if(canBeDataArray && wasArray) flags.calc = true; else changed(); } else if(valObject._isLinkedToArray) { var arrayEditIndices = []; var extraIndices = false; if(!inArray) flags.arrays[key] = arrayEditIndices; var minLen = Math.min(oldVal.length, newVal.length); var maxLen = Math.max(oldVal.length, newVal.length); if(minLen !== maxLen) { if(valObject.editType === 'arraydraw') { extraIndices = true; } else { changed(); continue; } } for(i = 0; i < minLen; i++) { getDiffFlags(oldVal[i], newVal[i], parts.concat(i), // add array indices, but not if we're already in an array Lib.extendFlat({inArray: key, arrayIndex: i}, opts)); } // put this at the end so that we know our collected array indices are sorted // but the check for length changes happens up front so we can short-circuit // diffing if appropriate if(extraIndices) { for(i = minLen; i < maxLen; i++) { arrayEditIndices.push(i); } } } else if(!valType && Lib.isPlainObject(oldVal)) { getDiffFlags(oldVal, newVal, parts, opts); } else if(canBeDataArray) { if(wasArray && nowArray) { // don't try to diff two data arrays. If immutable we know the data changed, // if not, assume it didn't and let `layout.datarevision` tell us if it did if(immutable) { flags.calc = true; } // look for animatable attributes when the data changed if(immutable || opts.newDataRevision) { changed(); } } else if(wasArray !== nowArray) { flags.calc = true; } else changed(); } else if(wasArray && nowArray) { // info array, colorscale, 'any' - these are short, just stringify. // I don't *think* that covers up any real differences post-validation, does it? // otherwise we need to dive in 1 (info_array) or 2 (colorscale) levels and compare // all elements. if(oldVal.length !== newVal.length || String(oldVal) !== String(newVal)) { changed(); } } else { changed(); } } for(key in newContainer) { if(!(key in oldContainer || key.charAt(0) === '_' || typeof newContainer[key] === 'function')) { valObject = getValObject(outerparts.concat(key)); if(valObjectCanBeDataArray(valObject) && Array.isArray(newContainer[key])) { flags.calc = true; return; } else changed(); } } } /* * simple diff for config - for now, just treat all changes as equivalent */ function diffConfig(oldConfig, newConfig) { var key; for(key in oldConfig) { if(key.charAt(0) === '_') continue; var oldVal = oldConfig[key]; var newVal = newConfig[key]; if(oldVal !== newVal) { if(Lib.isPlainObject(oldVal) && Lib.isPlainObject(newVal)) { if(diffConfig(oldVal, newVal)) { return true; } } else if(Array.isArray(oldVal) && Array.isArray(newVal)) { if(oldVal.length !== newVal.length) { return true; } for(var i = 0; i < oldVal.length; i++) { if(oldVal[i] !== newVal[i]) { if(Lib.isPlainObject(oldVal[i]) && Lib.isPlainObject(newVal[i])) { if(diffConfig(oldVal[i], newVal[i])) { return true; } } else { return true; } } } } else { return true; } } } } /** * 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 */ function animate(gd, frameOrGroupNameOrFrameList, animationOpts) { gd = Lib.getGraphDiv(gd); if(!Lib.isPlotDiv(gd)) { throw new Error( 'This element is not a Plotly plot: ' + gd + '. It\'s likely that you\'ve failed ' + 'to create a plot before animating it. For more details, see ' + 'https://plotly.com/javascript/animations/' ); } 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; } } // Execute a callback after the wrapper function has been called n times. // This is used to defer the resolution until a transition has resolved *and* // the frame has completed. If it's not done this way, then we get a race // condition in which the animation might resolve before a transition is complete // or vice versa. function callbackOnNthTime(cb, n) { var cnt = 0; return function() { if(cb && ++cnt === n) { return cb(); } }; } 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].type === 'byname') { // 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].data; } 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 = callbackOnNthTime(resolve, 2); 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(); } var newFrame = trans._currentFrame = trans._frameQueue.shift(); if(newFrame) { // Since it's sometimes necessary to do deep digging into frame data, // we'll consider it not 100% impossible for nulls or numbers to sneak through, // so check when casting the name, just to be absolutely certain: var stringName = newFrame.name ? newFrame.name.toString() : null; gd._fullLayout._currentFrame = stringName; 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 ).then(function() { if(newFrame.onComplete) { newFrame.onComplete(); } }); gd.emit('plotly_animatingframe', { name: stringName, frame: newFrame.frame, animation: { frame: newFrame.frameOpts, transition: 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) { // In this case, a simple object has been passed to animate. frameList.push({ type: 'object', data: setTransitionConfig(Lib.extendFlat({}, frameOrGroupNameOrFrameList)) }); } else if(allFrames || ['string', 'number'].indexOf(typeof frameOrGroupNameOrFrameList) !== -1) { // In this case, null or undefined has been passed so that we want to // animate *all* currently defined frames for(i = 0; i < trans._frames.length; i++) { frame = trans._frames[i]; if(!frame) continue; if(allFrames || String(frame.group) === String(frameOrGroupNameOrFrameList)) { frameList.push({ type: 'byname', name: String(frame.name), data: setTransitionConfig({name: frame.name}) }); } } } else if(isFrameArray) { for(i = 0; i < frameOrGroupNameOrFrameList.length; i++) { var frameOrName = frameOrGroupNameOrFrameList[i]; if(['number', 'string'].indexOf(typeof frameOrName) !== -1) { frameOrName = String(frameOrName); // In this case, there's an array and this frame is a string name: frameList.push({ type: 'byname', name: frameOrName, data: setTransitionConfig({name: frameOrName}) }); } else if(Lib.isPlainObject(frameOrName)) { frameList.push({ type: 'object', data: setTransitionConfig(Lib.extendFlat({}, frameOrName)) }); } } } // Verify that all of these frames actually exist; return and reject if not: for(i = 0; i < frameList.length; i++) { frame = frameList[i]; if(frame.type === 'byname' && !trans._frameHash[frame.data.name]) { Lib.warn('animate failure: frame not found: "' + frame.data.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(animationOpts.direction === 'reverse') { frameList.reverse(); } var currentFrame = gd._fullLayout._currentFrame; if(currentFrame && animationOpts.fromcurrent) { var idx = -1; for(i = 0; i < frameList.length; i++) { frame = frameList[i]; if(frame.type === 'byname' && frame.name === currentFrame) { idx = i; break; } } if(idx > 0 && idx < frameList.length - 1) { var filteredFrameList = []; for(i = 0; i < frameList.length; i++) { frame = frameList[i]; if(frameList[i].type !== 'byname' || i > idx) { filteredFrameList.push(frame); } } frameList = filteredFrameList; } } 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. */ function addFrames(gd, frameList, indices) { gd = Lib.getGraphDiv(gd); if(frameList === null || frameList === undefined) { return Promise.resolve(); } if(!Lib.isPlotDiv(gd)) { throw new Error( 'This element is not a Plotly plot: ' + gd + '. It\'s likely that you\'ve failed ' + 'to create a plot before adding frames. For more details, see ' + 'https://plotly.com/javascript/animations/' ); } var i, frame, j, idx; var _frames = gd._transitionData._frames; var _frameHash = 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 = []; var _frameHashLocal = {}; for(i = frameList.length - 1; i >= 0; i--) { if(!Lib.isPlainObject(frameList[i])) continue; // The entire logic for checking for this type of name collision can be removed once we migrate to ES6 and // use a Map instead of an Object instance, as Map keys aren't converted to strings. var lookupName = frameList[i].name; var name = (_frameHash[lookupName] || _frameHashLocal[lookupName] || {}).name; var newName = frameList[i].name; var collisionPresent = _frameHash[name] || _frameHashLocal[name]; if(name && newName && typeof newName === 'number' && collisionPresent && numericNameWarningCount < numericNameWarningCountLimit) { numericNameWarningCount++; Lib.warn('addFrames: overwriting frame "' + (_frameHash[name] || _frameHashLocal[name]).name + '" with a frame whose name of type "number" also equates to "' + name + '". This is valid but may potentially lead to unexpected ' + 'behavior since all plotly.js frame names are stored internally ' + 'as strings.'); if(numericNameWarningCount === numericNameWarningCountLimit) { Lib.warn('addFrames: This API call has yielded too many of these warnings. ' + 'For the rest of this call, further warnings about numeric frame ' + 'names will be suppressed.'); } } _frameHashLocal[lookupName] = {name: lookupName}; 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(typeof frame.name === 'number') { Lib.warn('Warning: addFrames accepts frames with numeric names, but the numbers are' + 'implicitly cast to strings'); } 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(_frameHash[(frame.name = 'frame ' + gd._transitionData._counter++)]); } if(_frameHash[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; var redoFunc = Plots.modifyFrames; var undoArgs = [gd, revops]; var 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 */ function deleteFrames(gd, frameList) { gd = Lib.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 = []; if(!frameList) { frameList = []; for(i = 0; i < _frames.length; i++) { frameList.push(i); } } frameList = frameList.slice(); 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; var redoFunc = Plots.modifyFrames; var undoArgs = [gd, revops]; var 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-_doPlot state * * @param {string id or DOM element} gd * the id or DOM element of the graph container div */ function purge(gd) { gd = Lib.getGraphDiv(gd); var fullLayout = gd._fullLayout || {}; var 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(); // in contrast to _doPlots.purge which does NOT clear _context! delete gd._context; return gd; } // determines if the graph div requires a recalculation of its inverse matrix transforms by comparing old + new bounding boxes. function calcInverseTransform(gd) { var fullLayout = gd._fullLayout; var newBBox = gd.getBoundingClientRect(); if(Lib.equalDomRects(newBBox, fullLayout._lastBBox)) return; var m = fullLayout._invTransform = Lib.inverseTransformMatrix(Lib.getFullTransformMatrix(gd)); fullLayout._invScaleX = Math.sqrt(m[0][0] * m[0][0] + m[0][1] * m[0][1] + m[0][2] * m[0][2]); fullLayout._invScaleY = Math.sqrt(m[1][0] * m[1][0] + m[1][1] * m[1][1] + m[1][2] * m[1][2]); fullLayout._lastBBox = newBBox; } // ------------------------------------------------------- // makePlotFramework: Create the plot container and axes // ------------------------------------------------------- function makePlotFramework(gd) { var gd3 = d3.select(gd); var fullLayout = gd._fullLayout; fullLayout._calcInverseTransform = calcInverseTransform; fullLayout._calcInverseTransform(gd); // 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('user-select-none', true) .classed('svg-container', true) .style('position', 'relative'); // 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 // FIXME: parcoords reuses this object, not the best pattern fullLayout._glcontainer = fullLayout._paperdiv.selectAll('.gl-container') .data([{}]); fullLayout._glcontainer.enter().append('div') .classed('gl-container', true); fullLayout._paperdiv.selectAll('.main-svg').remove(); fullLayout._paperdiv.select('.modebar-container').remove(); fullLayout._paper = fullLayout._paperdiv.insert('svg', ':first-child') .classed('main-svg', true); fullLayout._toppaper = fullLayout._paperdiv.append('svg') .classed('main-svg', true); fullLayout._modebardiv = fullLayout._paperdiv.append('div'); delete fullLayout._modeBar; fullLayout._hoverpaper = fullLayout._paperdiv.append('svg') .classed('main-svg', true); if(!fullLayout._uid) { var otherUids = {}; d3.selectAll('defs').each(function() { if(this.id) otherUids[this.id.split('-')[1]] = 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._clips = fullLayout._defs.append('g') .classed('clips', true); fullLayout._topdefs = fullLayout._toppaper.append('defs') .attr('id', 'topdefs-' + fullLayout._uid); fullLayout._topclips = fullLayout._topdefs.append('g') .classed('clips', true); fullLayout._bgLayer = fullLayout._paper.append('g') .classed('bglayer', true); fullLayout._draggers = fullLayout._paper.append('g') .classed('draglayer', true); // lower shape/image layer - note that this is behind // all subplots data/grids but above the backgrounds // except inset subplots, whose backgrounds are drawn // inside their own group so that they appear above // the data for the main subplot // lower shapes and images which are fully referenced to // a subplot still get drawn within the subplot's group // so they will work correctly on insets 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 polar layer for the whole plot fullLayout._polarlayer = fullLayout._paper.append('g').classed('polarlayer', true); // single smith layer for the whole plot fullLayout._smithlayer = fullLayout._paper.append('g').classed('smithlayer', true); // single ternary layer for the whole plot fullLayout._ternarylayer = fullLayout._paper.append('g').classed('ternarylayer', true); // single geo layer for the whole plot fullLayout._geolayer = fullLayout._paper.append('g').classed('geolayer', true); // single funnelarea layer for the whole plot fullLayout._funnelarealayer = fullLayout._paper.append('g').classed('funnelarealayer', true); // single pie layer for the whole plot fullLayout._pielayer = fullLayout._paper.append('g').classed('pielayer', true); // single treemap layer for the whole plot fullLayout._iciclelayer = fullLayout._paper.append('g').classed('iciclelayer', true); // single treemap layer for the whole plot fullLayout._treemaplayer = fullLayout._paper.append('g').classed('treemaplayer', true); // single sunburst layer for the whole plot fullLayout._sunburstlayer = fullLayout._paper.append('g').classed('sunburstlayer', true); // single indicator layer for the whole plot fullLayout._indicatorlayer = fullLayout._toppaper.append('g').classed('indicatorlayer', true); // fill in image server scrape-svg fullLayout._glimages = fullLayout._paper.append('g').classed('glimages', true); // lastly upper shapes, 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) // upper shapes/images are only those drawn above the whole plot, including subplots var layerAbove = fullLayout._toppaper.append('g') .classed('layer-above', true); fullLayout._imageUpperLayer = layerAbove.append('g') .classed('imagelayer', true); fullLayout._shapeUpperLayer = layerAbove.append('g') .classed('shapelayer', true); fullLayout._selectionLayer = fullLayout._toppaper.append('g').classed('selectionlayer', true); fullLayout._infolayer = fullLayout._toppaper.append('g').classed('infolayer', true); fullLayout._menulayer = fullLayout._toppaper.append('g').classed('menulayer', true); fullLayout._zoomlayer = fullLayout._toppaper.append('g').classed('zoomlayer', true); fullLayout._hoverlayer = fullLayout._hoverpaper.append('g').classed('hoverlayer', true); // Make the modebar container fullLayout._modebardiv .classed('modebar-container', true) .style('position', 'absolute') .style('top', '0px') .style('right', '0px'); gd.emit('plotly_framework'); } exports.animate = animate; exports.addFrames = addFrames; exports.deleteFrames = deleteFrames; exports.addTraces = addTraces; exports.deleteTraces = deleteTraces; exports.extendTraces = extendTraces; exports.moveTraces = moveTraces; exports.prependTraces = prependTraces; exports.newPlot = newPlot; exports._doPlot = _doPlot; exports.purge = purge; exports.react = react; exports.redraw = redraw; exports.relayout = relayout; exports.restyle = restyle; exports.setPlotConfig = setPlotConfig; exports.update = update; exports._guiRelayout = guiEdit(relayout); exports._guiRestyle = guiEdit(restyle); exports._guiUpdate = guiEdit(update); exports._storeDirectGUIEdit = _storeDirectGUIEdit;