diff --git a/draftlogs/7579_fix.md b/draftlogs/7579_fix.md new file mode 100644 index 00000000000..f0ab5a99fdf --- /dev/null +++ b/draftlogs/7579_fix.md @@ -0,0 +1 @@ +- Update config diff check method to handle nested arrays [[#7579](https://github.com/plotly/plotly.js/pull/7579)] diff --git a/src/plot_api/helpers.js b/src/plot_api/helpers.js index d57be1bc622..c1a16751ecc 100644 --- a/src/plot_api/helpers.js +++ b/src/plot_api/helpers.js @@ -14,8 +14,8 @@ var getFromTrace = AxisIds.getFromTrace; var traceIs = Registry.traceIs; // clear the promise queue if one of them got rejected -exports.clearPromiseQueue = function(gd) { - if(Array.isArray(gd._promises) && gd._promises.length > 0) { +exports.clearPromiseQueue = function (gd) { + if (Array.isArray(gd._promises) && gd._promises.length > 0) { Lib.log('Clearing previous rejected promises from queue.'); } @@ -25,22 +25,22 @@ exports.clearPromiseQueue = function(gd) { // make a few changes to the layout right away // before it gets used for anything // backward compatibility and cleanup of nonstandard options -exports.cleanLayout = function(layout) { +exports.cleanLayout = function (layout) { var i, j; - if(!layout) layout = {}; + if (!layout) layout = {}; // cannot have (x|y)axis1, numbering goes axis, axis2, axis3... - if(layout.xaxis1) { - if(!layout.xaxis) layout.xaxis = layout.xaxis1; + if (layout.xaxis1) { + if (!layout.xaxis) layout.xaxis = layout.xaxis1; delete layout.xaxis1; } - if(layout.yaxis1) { - if(!layout.yaxis) layout.yaxis = layout.yaxis1; + if (layout.yaxis1) { + if (!layout.yaxis) layout.yaxis = layout.yaxis1; delete layout.yaxis1; } - if(layout.scene1) { - if(!layout.scene) layout.scene = layout.scene1; + if (layout.scene1) { + if (!layout.scene) layout.scene = layout.scene1; delete layout.scene1; } @@ -50,85 +50,85 @@ exports.cleanLayout = function(layout) { var sceneAttrRegex = (Plots.subplotsRegistry.gl3d || {}).attrRegex; var keys = Object.keys(layout); - for(i = 0; i < keys.length; i++) { + for (i = 0; i < keys.length; i++) { var key = keys[i]; - if(axisAttrRegex && axisAttrRegex.test(key)) { + if (axisAttrRegex && axisAttrRegex.test(key)) { // modifications to cartesian axes var ax = layout[key]; - if(ax.anchor && ax.anchor !== 'free') { + if (ax.anchor && ax.anchor !== 'free') { ax.anchor = cleanId(ax.anchor); } - if(ax.overlaying) ax.overlaying = cleanId(ax.overlaying); + if (ax.overlaying) ax.overlaying = cleanId(ax.overlaying); // old method of axis type - isdate and islog (before category existed) - if(!ax.type) { - if(ax.isdate) ax.type = 'date'; - else if(ax.islog) ax.type = 'log'; - else if(ax.isdate === false && ax.islog === false) ax.type = 'linear'; + if (!ax.type) { + if (ax.isdate) ax.type = 'date'; + else if (ax.islog) ax.type = 'log'; + else if (ax.isdate === false && ax.islog === false) ax.type = 'linear'; } - if(ax.autorange === 'withzero' || ax.autorange === 'tozero') { + if (ax.autorange === 'withzero' || ax.autorange === 'tozero') { ax.autorange = true; ax.rangemode = 'tozero'; } - if(ax.insiderange) delete ax.range; + if (ax.insiderange) delete ax.range; delete ax.islog; delete ax.isdate; delete ax.categories; // replaced by _categories // prune empty domain arrays made before the new nestedProperty - if(emptyContainer(ax, 'domain')) delete ax.domain; + if (emptyContainer(ax, 'domain')) delete ax.domain; } } var annotationsLen = Array.isArray(layout.annotations) ? layout.annotations.length : 0; - for(i = 0; i < annotationsLen; i++) { + for (i = 0; i < annotationsLen; i++) { var ann = layout.annotations[i]; - if(!Lib.isPlainObject(ann)) continue; + if (!Lib.isPlainObject(ann)) continue; cleanAxRef(ann, 'xref'); cleanAxRef(ann, 'yref'); } var shapesLen = Array.isArray(layout.shapes) ? layout.shapes.length : 0; - for(i = 0; i < shapesLen; i++) { + for (i = 0; i < shapesLen; i++) { var shape = layout.shapes[i]; - if(!Lib.isPlainObject(shape)) continue; + if (!Lib.isPlainObject(shape)) continue; cleanAxRef(shape, 'xref'); cleanAxRef(shape, 'yref'); } var imagesLen = Array.isArray(layout.images) ? layout.images.length : 0; - for(i = 0; i < imagesLen; i++) { + for (i = 0; i < imagesLen; i++) { var image = layout.images[i]; - if(!Lib.isPlainObject(image)) continue; + if (!Lib.isPlainObject(image)) continue; cleanAxRef(image, 'xref'); cleanAxRef(image, 'yref'); } var legend = layout.legend; - if(legend) { + if (legend) { // check for old-style legend positioning (x or y is +/- 100) - if(legend.x > 3) { + if (legend.x > 3) { legend.x = 1.02; legend.xanchor = 'left'; - } else if(legend.x < -2) { + } else if (legend.x < -2) { legend.x = -0.02; legend.xanchor = 'right'; } - if(legend.y > 3) { + if (legend.y > 3) { legend.y = 1.02; legend.yanchor = 'bottom'; - } else if(legend.y < -2) { + } else if (legend.y < -2) { legend.y = -0.02; legend.yanchor = 'top'; } @@ -137,14 +137,14 @@ exports.cleanLayout = function(layout) { /* * Moved from rotate -> orbit for dragmode */ - if(layout.dragmode === 'rotate') layout.dragmode = 'orbit'; + if (layout.dragmode === 'rotate') layout.dragmode = 'orbit'; // sanitize rgb(fractions) and rgba(fractions) that old tinycolor // supported, but new tinycolor does not because they're not valid css Color.clean(layout); // clean the layout container in layout.template - if(layout.template && layout.template.layout) { + if (layout.template && layout.template.layout) { exports.cleanLayout(layout.template.layout); } @@ -154,7 +154,7 @@ exports.cleanLayout = function(layout) { function cleanAxRef(container, attr) { var valIn = container[attr]; var axLetter = attr.charAt(0); - if(valIn && valIn !== 'paper') { + if (valIn && valIn !== 'paper') { container[attr] = cleanId(valIn, axLetter, true); } } @@ -166,79 +166,79 @@ function cleanAxRef(container, attr) { * Important: if you're going to add something here that modifies a data array, * update it in place so the new array === the old one. */ -exports.cleanData = function(data) { - for(var tracei = 0; tracei < data.length; tracei++) { +exports.cleanData = function (data) { + for (var tracei = 0; tracei < data.length; tracei++) { var trace = data[tracei]; var i; // use xbins to bin data in x, and ybins to bin data in y - if(trace.type === 'histogramy' && 'xbins' in trace && !('ybins' in trace)) { + if (trace.type === 'histogramy' && 'xbins' in trace && !('ybins' in trace)) { trace.ybins = trace.xbins; delete trace.xbins; } // now we have only one 1D histogram type, and whether // it uses x or y data depends on trace.orientation - if(trace.type === 'histogramy') exports.swapXYData(trace); - if(trace.type === 'histogramx' || trace.type === 'histogramy') { + if (trace.type === 'histogramy') exports.swapXYData(trace); + if (trace.type === 'histogramx' || trace.type === 'histogramy') { trace.type = 'histogram'; } // scl->scale, reversescl->reversescale - if('scl' in trace && !('colorscale' in trace)) { + if ('scl' in trace && !('colorscale' in trace)) { trace.colorscale = trace.scl; delete trace.scl; } - if('reversescl' in trace && !('reversescale' in trace)) { + if ('reversescl' in trace && !('reversescale' in trace)) { trace.reversescale = trace.reversescl; delete trace.reversescl; } // axis ids x1 -> x, y1-> y - if(trace.xaxis) trace.xaxis = cleanId(trace.xaxis, 'x'); - if(trace.yaxis) trace.yaxis = cleanId(trace.yaxis, 'y'); + if (trace.xaxis) trace.xaxis = cleanId(trace.xaxis, 'x'); + if (trace.yaxis) trace.yaxis = cleanId(trace.yaxis, 'y'); // scene ids scene1 -> scene - if(traceIs(trace, 'gl3d') && trace.scene) { + if (traceIs(trace, 'gl3d') && trace.scene) { trace.scene = Plots.subplotsRegistry.gl3d.cleanId(trace.scene); } - if(!traceIs(trace, 'pie-like') && !traceIs(trace, 'bar-like')) { - if(Array.isArray(trace.textposition)) { - for(i = 0; i < trace.textposition.length; i++) { + if (!traceIs(trace, 'pie-like') && !traceIs(trace, 'bar-like')) { + if (Array.isArray(trace.textposition)) { + for (i = 0; i < trace.textposition.length; i++) { trace.textposition[i] = cleanTextPosition(trace.textposition[i]); } - } else if(trace.textposition) { + } else if (trace.textposition) { trace.textposition = cleanTextPosition(trace.textposition); } } // fix typo in colorscale definition var _module = Registry.getModule(trace); - if(_module && _module.colorbar) { + if (_module && _module.colorbar) { var containerName = _module.colorbar.container; var container = containerName ? trace[containerName] : trace; - if(container && container.colorscale) { - if(container.colorscale === 'YIGnBu') container.colorscale = 'YlGnBu'; - if(container.colorscale === 'YIOrRd') container.colorscale = 'YlOrRd'; + if (container && container.colorscale) { + if (container.colorscale === 'YIGnBu') container.colorscale = 'YlGnBu'; + if (container.colorscale === 'YIOrRd') container.colorscale = 'YlOrRd'; } } // fix typo in surface 'highlight*' definitions - if(trace.type === 'surface' && Lib.isPlainObject(trace.contours)) { + if (trace.type === 'surface' && Lib.isPlainObject(trace.contours)) { var dims = ['x', 'y', 'z']; - for(i = 0; i < dims.length; i++) { + for (i = 0; i < dims.length; i++) { var opts = trace.contours[dims[i]]; - if(!Lib.isPlainObject(opts)) continue; + if (!Lib.isPlainObject(opts)) continue; - if(opts.highlightColor) { + if (opts.highlightColor) { opts.highlightcolor = opts.highlightColor; delete opts.highlightColor; } - if(opts.highlightWidth) { + if (opts.highlightWidth) { opts.highlightwidth = opts.highlightWidth; delete opts.highlightWidth; } @@ -246,7 +246,7 @@ exports.cleanData = function(data) { } // fixes from converting finance from transforms to real trace types - if(trace.type === 'candlestick' || trace.type === 'ohlc') { + if (trace.type === 'candlestick' || trace.type === 'ohlc') { var increasingShowlegend = (trace.increasing || {}).showlegend !== false; var decreasingShowlegend = (trace.decreasing || {}).showlegend !== false; var increasingName = cleanFinanceDir(trace.increasing); @@ -254,27 +254,24 @@ exports.cleanData = function(data) { // now figure out something smart to do with the separate direction // names we removed - if((increasingName !== false) && (decreasingName !== false)) { + if (increasingName !== false && decreasingName !== false) { // both sub-names existed: base name previously had no effect // so ignore it and try to find a shared part of the sub-names - var newName = commonPrefix( - increasingName, decreasingName, - increasingShowlegend, decreasingShowlegend - ); + var newName = commonPrefix(increasingName, decreasingName, increasingShowlegend, decreasingShowlegend); // if no common part, leave whatever name was (or wasn't) there - if(newName) trace.name = newName; - } else if((increasingName || decreasingName) && !trace.name) { + if (newName) trace.name = newName; + } else if ((increasingName || decreasingName) && !trace.name) { // one sub-name existed but not the base name - just use the sub-name trace.name = increasingName || decreasingName; } } // prune empty containers made before the new nestedProperty - if(emptyContainer(trace, 'line')) delete trace.line; - if('marker' in trace) { - if(emptyContainer(trace.marker, 'line')) delete trace.marker.line; - if(emptyContainer(trace, 'marker')) delete trace.marker; + if (emptyContainer(trace, 'line')) delete trace.line; + if ('marker' in trace) { + if (emptyContainer(trace.marker, 'line')) delete trace.marker.line; + if (emptyContainer(trace, 'marker')) delete trace.marker; } // sanitize rgb(fractions) and rgba(fractions) that old tinycolor @@ -285,11 +282,11 @@ exports.cleanData = function(data) { // if false, this needs to happen in Histogram.calc because it // can be a one-time autobin so we need to know the results before // we can push them back into the trace. - if(trace.autobinx) { + if (trace.autobinx) { delete trace.autobinx; delete trace.xbins; } - if(trace.autobiny) { + if (trace.autobiny) { delete trace.autobiny; delete trace.ybins; } @@ -297,7 +294,7 @@ exports.cleanData = function(data) { }; function cleanFinanceDir(dirContainer) { - if(!Lib.isPlainObject(dirContainer)) return false; + if (!Lib.isPlainObject(dirContainer)) return false; var dirName = dirContainer.name; @@ -309,19 +306,19 @@ function cleanFinanceDir(dirContainer) { function commonPrefix(name1, name2, show1, show2) { // if only one is shown in the legend, use that - if(show1 && !show2) return name1; - if(show2 && !show1) return name2; + if (show1 && !show2) return name1; + if (show2 && !show1) return name2; // if both or neither are in the legend, check if one is blank (or whitespace) // and use the other one // note that hover labels can still use the name even if the legend doesn't - if(!name1.trim()) return name2; - if(!name2.trim()) return name1; + if (!name1.trim()) return name2; + if (!name2.trim()) return name1; var minLen = Math.min(name1.length, name2.length); var i; - for(i = 0; i < minLen; i++) { - if(name1.charAt(i) !== name2.charAt(i)) break; + for (i = 0; i < minLen; i++) { + if (name1.charAt(i) !== name2.charAt(i)) break; } var out = name1.substr(0, i); @@ -334,62 +331,60 @@ function cleanTextPosition(textposition) { var posY = 'middle'; var posX = 'center'; - if(typeof textposition === 'string') { - if(textposition.indexOf('top') !== -1) posY = 'top'; - else if(textposition.indexOf('bottom') !== -1) posY = 'bottom'; + if (typeof textposition === 'string') { + if (textposition.indexOf('top') !== -1) posY = 'top'; + else if (textposition.indexOf('bottom') !== -1) posY = 'bottom'; - if(textposition.indexOf('left') !== -1) posX = 'left'; - else if(textposition.indexOf('right') !== -1) posX = 'right'; + if (textposition.indexOf('left') !== -1) posX = 'left'; + else if (textposition.indexOf('right') !== -1) posX = 'right'; } return posY + ' ' + posX; } function emptyContainer(outer, innerStr) { - return (innerStr in outer) && - (typeof outer[innerStr] === 'object') && - (Object.keys(outer[innerStr]).length === 0); + return innerStr in outer && typeof outer[innerStr] === 'object' && Object.keys(outer[innerStr]).length === 0; } - // swap all the data and data attributes associated with x and y -exports.swapXYData = function(trace) { +exports.swapXYData = function (trace) { var i; Lib.swapAttrs(trace, ['?', '?0', 'd?', '?bins', 'nbins?', 'autobin?', '?src', 'error_?']); - if(Array.isArray(trace.z) && Array.isArray(trace.z[0])) { - if(trace.transpose) delete trace.transpose; + if (Array.isArray(trace.z) && Array.isArray(trace.z[0])) { + if (trace.transpose) delete trace.transpose; else trace.transpose = true; } - if(trace.error_x && trace.error_y) { + if (trace.error_x && trace.error_y) { var errorY = trace.error_y; - var copyYstyle = ('copy_ystyle' in errorY) ? - errorY.copy_ystyle : - !(errorY.color || errorY.thickness || errorY.width); + var copyYstyle = + 'copy_ystyle' in errorY ? errorY.copy_ystyle : !(errorY.color || errorY.thickness || errorY.width); Lib.swapAttrs(trace, ['error_?.copy_ystyle']); - if(copyYstyle) { + if (copyYstyle) { Lib.swapAttrs(trace, ['error_?.color', 'error_?.thickness', 'error_?.width']); } } - if(typeof trace.hoverinfo === 'string') { + if (typeof trace.hoverinfo === 'string') { var hoverInfoParts = trace.hoverinfo.split('+'); - for(i = 0; i < hoverInfoParts.length; i++) { - if(hoverInfoParts[i] === 'x') hoverInfoParts[i] = 'y'; - else if(hoverInfoParts[i] === 'y') hoverInfoParts[i] = 'x'; + for (i = 0; i < hoverInfoParts.length; i++) { + if (hoverInfoParts[i] === 'x') hoverInfoParts[i] = 'y'; + else if (hoverInfoParts[i] === 'y') hoverInfoParts[i] = 'x'; } trace.hoverinfo = hoverInfoParts.join('+'); } }; // coerce traceIndices input to array of trace indices -exports.coerceTraceIndices = function(gd, traceIndices) { - if(isNumeric(traceIndices)) { +exports.coerceTraceIndices = function (gd, traceIndices) { + if (isNumeric(traceIndices)) { return [traceIndices]; - } else if(!Array.isArray(traceIndices) || !traceIndices.length) { - return gd.data.map(function(_, i) { return i; }); - } else if(Array.isArray(traceIndices)) { + } else if (!Array.isArray(traceIndices) || !traceIndices.length) { + return gd.data.map(function (_, i) { + return i; + }); + } else if (Array.isArray(traceIndices)) { var traceIndicesOut = []; - for(var i = 0; i < traceIndices.length; i++) { - if(Lib.isIndex(traceIndices[i], gd.data.length)) { + for (var i = 0; i < traceIndices.length; i++) { + if (Lib.isIndex(traceIndices[i], gd.data.length)) { traceIndicesOut.push(traceIndices[i]); } else { Lib.warn('trace index (', traceIndices[i], ') is not a number or is out of bounds'); @@ -413,7 +408,7 @@ exports.coerceTraceIndices = function(gd, traceIndices) { * undo hash (N.B. undoit may be mutated here). * */ -exports.manageArrayContainers = function(np, newVal, undoit) { +exports.manageArrayContainers = function (np, newVal, undoit) { var obj = np.obj; var parts = np.parts; var pLength = parts.length; @@ -421,7 +416,7 @@ exports.manageArrayContainers = function(np, newVal, undoit) { var pLastIsNumber = isNumeric(pLast); - if(pLastIsNumber && newVal === null) { + if (pLastIsNumber && newVal === null) { // delete item // Clear item in array container when new value is null @@ -431,11 +426,11 @@ exports.manageArrayContainers = function(np, newVal, undoit) { // Note that nested property clears null / undefined at end of // array container, but not within them. - } else if(pLastIsNumber && np.get() === undefined) { + } else if (pLastIsNumber && np.get() === undefined) { // create item // When adding a new item, make sure undo command will remove it - if(np.get() === undefined) undoit[np.astr] = null; + if (np.get() === undefined) undoit[np.astr] = null; np.set(newVal); } else { @@ -457,10 +452,10 @@ var ATTR_TAIL_RE = /(\.[^\[\]\.]+|\[[^\[\]\.]+\])$/; function getParent(attr) { var tail = attr.search(ATTR_TAIL_RE); - if(tail > 0) return attr.substr(0, tail); + if (tail > 0) return attr.substr(0, tail); } -/* +/** * hasParent: does an attribute object contain a parent of the given attribute? * for example, given 'images[2].x' do we also have 'images' or 'images[2]'? * @@ -471,15 +466,16 @@ function getParent(attr) { * @returns {Boolean} * is a parent of attr present in aobj? */ -exports.hasParent = function(aobj, attr) { +exports.hasParent = function (aobj, attr) { var attrParent = getParent(attr); - while(attrParent) { - if(attrParent in aobj) return true; + while (attrParent) { + if (attrParent in aobj) return true; attrParent = getParent(attrParent); } return false; }; +const AX_LETTERS = ['x', 'y', 'z']; /** * Empty out types for all axes containing these traces so we auto-set them again * @@ -488,27 +484,58 @@ exports.hasParent = function(aobj, attr) { * @param {object} layoutUpdate: any update being done concurrently to the layout, * which may supercede clearing the axis types */ -var axLetters = ['x', 'y', 'z']; -exports.clearAxisTypes = function(gd, traces, layoutUpdate) { - for(var i = 0; i < traces.length; i++) { +exports.clearAxisTypes = function (gd, traces, layoutUpdate) { + for (var i = 0; i < traces.length; i++) { var trace = gd._fullData[i]; - for(var j = 0; j < 3; j++) { - var ax = getFromTrace(gd, trace, axLetters[j]); + for (var j = 0; j < 3; j++) { + var ax = getFromTrace(gd, trace, AX_LETTERS[j]); // do not clear log type - that's never an auto result so must have been intentional - if(ax && ax.type !== 'log') { + if (ax && ax.type !== 'log') { var axAttr = ax._name; var sceneName = ax._id.substr(1); - if(sceneName.substr(0, 5) === 'scene') { - if(layoutUpdate[sceneName] !== undefined) continue; + if (sceneName.substr(0, 5) === 'scene') { + if (layoutUpdate[sceneName] !== undefined) continue; axAttr = sceneName + '.' + axAttr; } var typeAttr = axAttr + '.type'; - if(layoutUpdate[axAttr] === undefined && layoutUpdate[typeAttr] === undefined) { + if (layoutUpdate[axAttr] === undefined && layoutUpdate[typeAttr] === undefined) { Lib.nestedProperty(gd.layout, typeAttr).set(null); } } } } }; + +/** + * Check if a collection (object or array) has changed given two versions of + * the collection: old and new. + * + * @param {Object|Array} oldCollection: Old version of collection to compare + * @param {Object|Array} newCollection: New version of collection to compare + */ +const hasCollectionChanged = (oldCollection, newCollection) => { + const isArrayOrObject = (...vals) => vals.every((v) => Lib.isPlainObject(v)) || vals.every((v) => Array.isArray(v)); + if ([oldCollection, newCollection].every((a) => Array.isArray(a))) { + if (oldCollection.length !== newCollection.length) return true; + + for (let i = 0; i < oldCollection.length; i++) { + const oldVal = oldCollection[i]; + const newVal = newCollection[i]; + if (oldVal !== newVal) return isArrayOrObject(oldVal, newVal) ? hasCollectionChanged(oldVal, newVal) : true; + } + } else { + if (Object.keys(oldCollection).length !== Object.keys(newCollection).length) return true; + + for (const k in oldCollection) { + if (k.startsWith('_')) return false; + const oldVal = oldCollection[k]; + const newVal = newCollection[k]; + if (oldVal !== newVal) return isArrayOrObject(oldVal, newVal) ? hasCollectionChanged(oldVal, newVal) : true; + } + } + + return false; +}; +exports.hasCollectionChanged = hasCollectionChanged; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 83cd9cbc9ee..99a90e271f4 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -64,7 +64,7 @@ function _doPlot(gd, data, layout, config) { // Events.init is idempotent and bails early if gd has already been init'd Events.init(gd); - if(Lib.isPlainObject(data)) { + if (Lib.isPlainObject(data)) { var obj = data; data = obj.data; layout = obj.layout; @@ -73,17 +73,16 @@ function _doPlot(gd, data, layout, config) { } var okToPlot = Events.triggerHandler(gd, 'plotly_beforeplot', [data, layout, config]); - if(okToPlot === false) return Promise.reject(); + 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); + 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) { + if (frames) { return exports.addFrames(gd, frames); } } @@ -92,7 +91,7 @@ function _doPlot(gd, data, layout, config) { // a more OO like model setPlotContext(gd, config); - if(!layout) layout = {}; + if (!layout) layout = {}; // hook class for plots main container (in case of plotly.js // this won't be #embedded-graph or .js-tab-contents) @@ -107,16 +106,16 @@ function _doPlot(gd, data, layout, config) { // 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 = []; + if (!Array.isArray(gd._promises)) gd._promises = []; - var graphWasEmpty = ((gd.data || []).length === 0 && Array.isArray(data)); + 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)) { + if (Array.isArray(data)) { helpers.cleanData(data); - if(graphWasEmpty) gd.data = data; + if (graphWasEmpty) gd.data = data; else gd.data.push.apply(gd.data, data); // for routines outside graph_obj that want a clean tab @@ -125,7 +124,7 @@ function _doPlot(gd, data, layout, config) { gd.empty = false; } - if(!gd.layout || graphWasEmpty) { + if (!gd.layout || graphWasEmpty) { gd.layout = helpers.cleanLayout(layout); } @@ -139,10 +138,10 @@ function _doPlot(gd, data, layout, config) { fullLayout._replotting = true; // make or remake the framework if we need to - if(graphWasEmpty || fullLayout._shouldCreateBgLayer) { + if (graphWasEmpty || fullLayout._shouldCreateBgLayer) { makePlotFramework(gd); - if(fullLayout._shouldCreateBgLayer) { + if (fullLayout._shouldCreateBgLayer) { delete fullLayout._shouldCreateBgLayer; } } @@ -152,25 +151,27 @@ function _doPlot(gd, data, layout, config) { Drawing.initPatterns(gd); // save initial show spikes once per graph - if(graphWasEmpty) Axes.saveShowSpikeInitial(gd); + 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); + if (recalc) Plots.doCalcdata(gd); // in case it has changed, attach fullData traces to calcdata - for(var i = 0; i < gd.calcdata.length; i++) { + 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) { + 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); }; + gd._responsiveChartHandler = function () { + if (!Lib.isHidden(gd)) Plots.resize(gd); + }; // Listen to window resize window.addEventListener('resize', gd._responsiveChartHandler); @@ -191,29 +192,40 @@ function _doPlot(gd, data, layout, config) { function drawFramework() { var basePlotModules = fullLayout._basePlotModules; - for(var i = 0; i < basePlotModules.length; i++) { - if(basePlotModules[i].drawFramework) { + 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) { + 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({ @@ -226,7 +238,7 @@ function _doPlot(gd, data, layout, config) { } var plotGlPixelRatio = gd._context.plotGlPixelRatio; - if(fullLayout._glcanvas) { + if (fullLayout._glcanvas) { fullLayout._glcanvas .attr('width', fullLayout.width * plotGlPixelRatio) .attr('height', fullLayout.height * plotGlPixelRatio) @@ -234,14 +246,15 @@ function _doPlot(gd, data, layout, config) { .style('height', fullLayout.height + 'px'); var regl = fullLayout._glcanvas.data()[0].regl; - if(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 || + 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) { + if (drawFrameworkCalls) { Lib.error(msg); } else { Lib.log(msg + ' Clearing graph and plotting again.'); @@ -256,14 +269,10 @@ function _doPlot(gd, data, layout, config) { } } - if(fullLayout.modebar.orientation === 'h') { - fullLayout._modebardiv - .style('height', null) - .style('width', '100%'); + if (fullLayout.modebar.orientation === 'h') { + fullLayout._modebardiv.style('height', null).style('width', '100%'); } else { - fullLayout._modebardiv - .style('width', null) - .style('height', fullLayout.height + 'px'); + fullLayout._modebardiv.style('width', null).style('height', fullLayout.height + 'px'); } return Plots.previousPromises(gd); @@ -280,14 +289,14 @@ function _doPlot(gd, data, layout, config) { subroutines.drawMarginPushers(gd); Axes.allowAutoMargin(gd); - if(gd._fullLayout.title.text && gd._fullLayout.title.automargin) Plots.allowAutoMargin(gd, 'title.automargin'); + if (gd._fullLayout.title.text && gd._fullLayout.title.automargin) Plots.allowAutoMargin(gd, 'title.automargin'); // TODO can this be moved elsewhere? - if(fullLayout._has('pie')) { + if (fullLayout._has('pie')) { var fullData = gd._fullData; - for(var i = 0; i < fullData.length; i++) { + for (var i = 0; i < fullData.length; i++) { var trace = fullData[i]; - if(trace.type === 'pie' && trace.automargin) { + if (trace.type === 'pie' && trace.automargin) { Plots.allowAutoMargin(gd, 'pie.' + trace.uid + '.automargin'); } } @@ -299,37 +308,37 @@ function _doPlot(gd, data, layout, config) { // in case the margins changed, draw margin pushers again function marginPushersAgain() { - if(!Plots.didMarginChange(oldMargins, fullLayout._size)) return; + if (!Plots.didMarginChange(oldMargins, fullLayout._size)) return; - return Lib.syncOrAsync([ - marginPushers, - subroutines.layoutStyles - ], gd); + return Lib.syncOrAsync([marginPushers, subroutines.layoutStyles], gd); } function positionAndAutorange() { - if(!recalc) { + 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); + return Lib.syncOrAsync( + [ + Registry.getComponentMethod('shapes', 'calcAutorange'), + Registry.getComponentMethod('annotations', 'calcAutorange'), + doAutoRangeAndConstraints + ], + gd + ); } function doAutoRangeAndConstraints() { - if(gd._transitioning) return; + 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); + if (graphWasEmpty) Axes.saveRangeInitial(gd); // this one is different from shapes/annotations calcAutorange // the others incorporate those components into ax._extremes, @@ -342,31 +351,22 @@ function _doPlot(gd, data, layout, config) { return Axes.draw(gd, graphWasEmpty ? '' : 'redraw'); } - var seq = [ - Plots.previousPromises, - addFrames, - drawFramework, - marginPushers, - marginPushersAgain - ]; + var seq = [Plots.previousPromises, addFrames, drawFramework, marginPushers, marginPushersAgain]; - if(hasCartesian) seq.push(positionAndAutorange); + 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); - }); - } + 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( @@ -388,9 +388,9 @@ function _doPlot(gd, data, layout, config) { // 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(); + if (!plotDone || !plotDone.then) plotDone = Promise.resolve(); - return plotDone.then(function() { + return plotDone.then(function () { emitAfterPlot(gd); return gd; }); @@ -399,7 +399,7 @@ function _doPlot(gd, data, layout, config) { function emitAfterPlot(gd) { var fullLayout = gd._fullLayout; - if(fullLayout._redrawFromAutoMarginCount) { + if (fullLayout._redrawFromAutoMarginCount) { fullLayout._redrawFromAutoMarginCount--; } else { gd.emit('plotly_afterplot'); @@ -413,7 +413,7 @@ function setPlotConfig(obj) { function setBackground(gd, bgColor) { try { gd._fullLayout._paper.style('background', bgColor); - } catch(e) { + } catch (e) { Lib.error(e); } } @@ -424,27 +424,25 @@ function opaqueSetBackground(gd, bgColor) { } function setPlotContext(gd, config) { - if(!gd._context) { + 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] : - ''; + gd._context._baseUrl = base.size() && base.attr('href') ? window.location.href.split('#')[0] : ''; } var context = gd._context; var i, keys, key; - if(config) { + if (config) { keys = Object.keys(config); - for(i = 0; i < keys.length; i++) { + 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') { + if (key === 'editable' || key === 'edits') continue; + if (key in context) { + if (key === 'setBackground' && config[key] === 'opaque') { context[key] = opaqueSetBackground; } else { context[key] = config[key]; @@ -455,21 +453,21 @@ function setPlotContext(gd, config) { // now deal with editable and edits - first editable overrides // everything, then edits refines var editable = config.editable; - if(editable !== undefined) { + 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++) { + for (i = 0; i < keys.length; i++) { context.edits[keys[i]] = editable; } } - if(config.edits) { + if (config.edits) { keys = Object.keys(config.edits); - for(i = 0; i < keys.length; i++) { + for (i = 0; i < keys.length; i++) { key = keys[i]; - if(key in context.edits) { + if (key in context.edits) { context.edits[key] = config.edits[key]; } } @@ -480,7 +478,7 @@ function setPlotContext(gd, config) { } // staticPlot forces a bunch of others: - if(context.staticPlot) { + if (context.staticPlot) { context.editable = false; context.edits = {}; context.autosizable = false; @@ -492,12 +490,12 @@ function setPlotContext(gd, config) { } // make sure hover-only devices have mode bar visible - if(context.displayModeBar === 'hover' && !hasHover) { + if (context.displayModeBar === 'hover' && !hasHover) { context.displayModeBar = true; } // default and fallback for setBackground - if(context.setBackground === 'transparent' || typeof context.setBackground !== 'function') { + if (context.setBackground === 'transparent' || typeof context.setBackground !== 'function') { context.setBackground = setBackground; } @@ -507,19 +505,19 @@ function setPlotContext(gd, config) { // fill context._scrollZoom helper to help manage scrollZoom flaglist var szIn = context.scrollZoom; - var szOut = context._scrollZoom = {}; - if(szIn === true) { + 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') { + } else if (typeof szIn === 'string') { var parts = szIn.split('+'); - for(i = 0; i < parts.length; i++) { + for (i = 0; i < parts.length; i++) { szOut[parts[i]] = 1; } - } else if(szIn !== false) { + } else if (szIn !== false) { szOut.gl3d = 1; szOut.geo = 1; szOut.mapbox = 1; @@ -527,12 +525,11 @@ function setPlotContext(gd, config) { } } - // convenience function to force a full redraw, mostly for use by plotly.js function redraw(gd) { gd = Lib.getGraphDiv(gd); - if(!Lib.isPlotDiv(gd)) { + if (!Lib.isPlotDiv(gd)) { throw new Error('This element is not a Plotly plot: ' + gd); } @@ -540,7 +537,7 @@ function redraw(gd) { helpers.cleanLayout(gd.layout); gd.calcdata = undefined; - return exports._doPlot(gd).then(function() { + return exports._doPlot(gd).then(function () { gd.emit('plotly_redraw'); return gd; }); @@ -576,9 +573,9 @@ function positivifyIndices(indices, maxIndex) { var i; var index; - for(i = 0; i < indices.length; i++) { + for (i = 0; i < indices.length; i++) { index = indices[i]; - if(index < 0) { + if (index < 0) { positiveIndices.push(parentLength + index); } else { positiveIndices.push(index); @@ -597,26 +594,27 @@ function positivifyIndices(indices, maxIndex) { * @param arrayName */ function assertIndexArray(gd, indices, arrayName) { - var i, - index; + var i, index; - for(i = 0; i < indices.length; i++) { + for (i = 0; i < indices.length; i++) { index = indices[i]; // validate that indices are indeed integers - if(index !== parseInt(index, 10)) { + 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) { + 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) { + 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.'); } } @@ -631,28 +629,28 @@ function assertIndexArray(gd, indices, arrayName) { */ function checkMoveTracesArgs(gd, currentIndices, newIndices) { // check that gd has attribute 'data' and 'data' is array - if(!Array.isArray(gd.data)) { + if (!Array.isArray(gd.data)) { throw new Error('gd.data must be an array.'); } // validate currentIndices array - if(typeof currentIndices === 'undefined') { + if (typeof currentIndices === 'undefined') { throw new Error('currentIndices is a required argument.'); - } else if(!Array.isArray(currentIndices)) { + } else if (!Array.isArray(currentIndices)) { currentIndices = [currentIndices]; } assertIndexArray(gd, currentIndices, 'currentIndices'); // validate newIndices array if it exists - if(typeof newIndices !== 'undefined' && !Array.isArray(newIndices)) { + if (typeof newIndices !== 'undefined' && !Array.isArray(newIndices)) { newIndices = [newIndices]; } - if(typeof newIndices !== 'undefined') { + 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) { + if (typeof newIndices !== 'undefined' && currentIndices.length !== newIndices.length) { throw new Error('current and new indices must be of equal length.'); } } @@ -667,36 +665,34 @@ function checkAddTracesArgs(gd, traces, newIndices) { var i, value; // check that gd has attribute 'data' and 'data' is array - if(!Array.isArray(gd.data)) { + if (!Array.isArray(gd.data)) { throw new Error('gd.data must be an array.'); } // make sure traces exists - if(typeof traces === 'undefined') { + if (typeof traces === 'undefined') { throw new Error('traces must be defined.'); } // make sure traces is an array - if(!Array.isArray(traces)) { + if (!Array.isArray(traces)) { traces = [traces]; } // make sure each value in traces is an object - for(i = 0; i < traces.length; i++) { + for (i = 0; i < traces.length; i++) { value = traces[i]; - if(typeof value !== 'object' || (Array.isArray(value) || value === null)) { + 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)) { + 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' - ); + if (typeof newIndices !== 'undefined' && newIndices.length !== traces.length) { + throw new Error('if indices is specified, traces.length must equal indices.length'); } } @@ -713,36 +709,39 @@ function checkAddTracesArgs(gd, traces, newIndices) { function assertExtendTracesArgs(gd, update, indices, maxPoints) { var maxPointsIsObject = Lib.isPlainObject(maxPoints); - if(!Array.isArray(gd.data)) { + if (!Array.isArray(gd.data)) { throw new Error('gd.data must be an array'); } - if(!Lib.isPlainObject(update)) { + if (!Lib.isPlainObject(update)) { throw new Error('update must be a key:value object'); } - if(typeof indices === 'undefined') { + 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) { + 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) { + 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 ' + - 'correspondence with the keys and number of traces in the update object'); + 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 ' + + 'correspondence with the keys and number of traces in the update object' + ); } } } @@ -762,14 +761,14 @@ function getExtendProperties(gd, update, indices, maxPoints) { var trace, target, prop, insert, maxp; // allow scalar index to represent a single trace position - if(!Array.isArray(indices)) indices = [indices]; + 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++) { + 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. @@ -784,13 +783,13 @@ function getExtendProperties(gd, update, indices, maxPoints) { target = prop.get(); insert = update[key][j]; - if(!Lib.isArrayOrTypedArray(insert)) { + if (!Lib.isArrayOrTypedArray(insert)) { throw new Error('attribute: ' + key + ' index: ' + j + ' must be an array'); } - if(!Lib.isArrayOrTypedArray(target)) { + if (!Lib.isArrayOrTypedArray(target)) { throw new Error('cannot extend missing or non-array attribute: ' + key); } - if(target.constructor !== insert.constructor) { + if (target.constructor !== insert.constructor) { throw new Error('cannot extend array with an array of a different type: ' + key); } @@ -801,7 +800,7 @@ function getExtendProperties(gd, update, indices, maxPoints) { 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; + if (!isNumeric(maxp)) maxp = -1; /* * Wrap the nestedProperty in an object containing required data @@ -838,7 +837,7 @@ function spliceTraces(gd, update, indices, maxPoints, updateArray) { var undoUpdate = {}; var undoPoints = {}; - for(var i = 0; i < updateProps.length; i++) { + for (var i = 0; i < updateProps.length; i++) { var prop = updateProps[i].prop; var maxp = updateProps[i].maxp; @@ -847,15 +846,15 @@ function spliceTraces(gd, update, indices, maxPoints, updateArray) { prop.set(out[0]); // build the inverse update object for the undo operation - if(!Array.isArray(undoUpdate[prop.astr])) undoUpdate[prop.astr] = []; + 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] = []; + // 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}; + return { update: undoUpdate, maxPoints: undoPoints }; } function concatTypedArray(arr0, arr1) { @@ -888,12 +887,12 @@ function extendTraces(gd, update, indices, maxPoints) { function updateArray(target, insert, maxp) { var newArray, remainder; - if(Lib.isTypedArray(target)) { - if(maxp < 0) { + if (Lib.isTypedArray(target)) { + if (maxp < 0) { var none = new target.constructor(0); var both = concatTypedArray(target, insert); - if(maxp < 0) { + if (maxp < 0) { newArray = both; remainder = none; } else { @@ -904,10 +903,10 @@ function extendTraces(gd, update, indices, maxPoints) { newArray = new target.constructor(maxp); remainder = new target.constructor(target.length + insert.length - maxp); - if(maxp === insert.length) { + if (maxp === insert.length) { newArray.set(insert); remainder.set(target); - } else if(maxp < insert.length) { + } else if (maxp < insert.length) { var numberOfItemsFromInsert = insert.length - maxp; newArray.set(insert.subarray(numberOfItemsFromInsert)); @@ -924,9 +923,7 @@ function extendTraces(gd, update, indices, maxPoints) { } } else { newArray = target.concat(insert); - remainder = (maxp >= 0 && maxp < newArray.length) ? - newArray.splice(0, newArray.length - maxp) : - []; + remainder = maxp >= 0 && maxp < newArray.length ? newArray.splice(0, newArray.length - maxp) : []; } return [newArray, remainder]; @@ -946,12 +943,12 @@ function prependTraces(gd, update, indices, maxPoints) { function updateArray(target, insert, maxp) { var newArray, remainder; - if(Lib.isTypedArray(target)) { - if(maxp <= 0) { + if (Lib.isTypedArray(target)) { + if (maxp <= 0) { var none = new target.constructor(0); var both = concatTypedArray(insert, target); - if(maxp < 0) { + if (maxp < 0) { newArray = both; remainder = none; } else { @@ -962,10 +959,10 @@ function prependTraces(gd, update, indices, maxPoints) { newArray = new target.constructor(maxp); remainder = new target.constructor(target.length + insert.length - maxp); - if(maxp === insert.length) { + if (maxp === insert.length) { newArray.set(insert); remainder.set(target); - } else if(maxp < insert.length) { + } else if (maxp < insert.length) { var numberOfItemsFromInsert = insert.length - maxp; newArray.set(insert.subarray(0, numberOfItemsFromInsert)); @@ -981,9 +978,7 @@ function prependTraces(gd, update, indices, maxPoints) { } } else { newArray = insert.concat(target); - remainder = (maxp >= 0 && maxp < newArray.length) ? - newArray.splice(maxp, newArray.length) : - []; + remainder = maxp >= 0 && maxp < newArray.length ? newArray.splice(maxp, newArray.length) : []; } return [newArray, remainder]; @@ -1013,7 +1008,7 @@ function addTraces(gd, traces, newIndices) { var undoFunc = exports.deleteTraces; var redoFunc = addTraces; var undoArgs = [gd, currentIndices]; - var redoArgs = [gd, traces]; // no newIndices here + var redoArgs = [gd, traces]; // no newIndices here var i; var promise; @@ -1021,44 +1016,44 @@ function addTraces(gd, traces, newIndices) { checkAddTracesArgs(gd, traces, newIndices); // make sure traces is an array - if(!Array.isArray(traces)) { + if (!Array.isArray(traces)) { traces = [traces]; } // make sure traces do not repeat existing ones - traces = traces.map(function(trace) { + 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++) { + 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++) { + 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') { + 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)) { + if (!Array.isArray(newIndices)) { newIndices = [newIndices]; } try { // this is redundant, but necessary to not catch later possible errors! checkMoveTracesArgs(gd, currentIndices, newIndices); - } catch(error) { + } 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; @@ -1092,9 +1087,9 @@ function deleteTraces(gd, indices) { var deletedTrace; // make sure indices are defined - if(typeof indices === 'undefined') { + if (typeof indices === 'undefined') { throw new Error('indices must be an integer or array of integers.'); - } else if(!Array.isArray(indices)) { + } else if (!Array.isArray(indices)) { indices = [indices]; } assertIndexArray(gd, indices, 'indices'); @@ -1104,7 +1099,7 @@ function deleteTraces(gd, indices) { // we want descending here so that splicing later doesn't affect indexing indices.sort(Lib.sorterDes); - for(i = 0; i < indices.length; i += 1) { + for (i = 0; i < indices.length; i += 1) { deletedTrace = gd.data.splice(indices[i], 1)[0]; traces.push(deletedTrace); } @@ -1165,9 +1160,9 @@ function moveTraces(gd, currentIndices, newIndices) { currentIndices = Array.isArray(currentIndices) ? currentIndices : [currentIndices]; // if undefined, define newIndices to point to the end of gd.data array - if(typeof newIndices === 'undefined') { + if (typeof newIndices === 'undefined') { newIndices = []; - for(i = 0; i < currentIndices.length; i++) { + for (i = 0; i < currentIndices.length; i++) { newIndices.push(-currentIndices.length + i); } } @@ -1182,25 +1177,25 @@ function moveTraces(gd, currentIndices, newIndices) { // 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++) { + for (i = 0; i < gd.data.length; i++) { // if index isn't in currentIndices, include it in ignored! - if(currentIndices.indexOf(i) === -1) { + 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]]}); + 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) { + 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) { + for (i = 0; i < movingTraceMap.length; i += 1) { newData.splice(movingTraceMap[i].newIndex, 0, movingTraceMap[i].trace); } @@ -1247,17 +1242,17 @@ function restyle(gd, astr, val, _traces) { helpers.clearPromiseQueue(gd); var aobj = {}; - if(typeof astr === 'string') aobj[astr] = val; - else if(Lib.isPlainObject(astr)) { + 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; + if (_traces === undefined) _traces = val; } else { Lib.warn('Restyle fail.', astr, val, _traces); return Promise.reject(); } - if(Object.keys(aobj).length) gd.changed = true; + if (Object.keys(aobj).length) gd.changed = true; var traces = helpers.coerceTraceIndices(gd, _traces); @@ -1265,13 +1260,13 @@ function restyle(gd, astr, val, _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, {}); + if (flags.calc) gd.calcdata = undefined; + if (flags.clearAxisTypes) helpers.clearAxisTypes(gd, traces, {}); // fill in redraw sequence var seq = []; - if(flags.fullReplot) { + if (flags.fullReplot) { seq.push(exports._doPlot); } else { seq.push(Plots.previousPromises); @@ -1280,7 +1275,7 @@ function restyle(gd, astr, val, _traces) { // to skip over long and slow axes defaults Plots.supplyDefaults(gd); - if(flags.markerSize) { + if (flags.markerSize) { Plots.doCalcdata(gd); addAxRangeSequence(seq); @@ -1291,27 +1286,20 @@ function restyle(gd, astr, val, _traces) { // which in general must redraws 'all' axes } - if(flags.style) seq.push(subroutines.doTraceStyle); - if(flags.colorbars) seq.push(subroutines.doColorBars); + 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 - ); + seq.push(Plots.rehover, Plots.redrag, Plots.reselect); - Queue.add(gd, - restyle, [gd, specs.undoit, specs.traces], - restyle, [gd, specs.redoit, specs.traces] - ); + 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(); + if (!plotDone || !plotDone.then) plotDone = Promise.resolve(); - return plotDone.then(function() { + return plotDone.then(function () { gd.emit('plotly_restyle', specs.eventData); return gd; }); @@ -1320,7 +1308,7 @@ function restyle(gd, astr, val, _traces) { // 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; + if (val === undefined) return null; return val; } @@ -1330,12 +1318,12 @@ function undefinedToNull(val) { * to prepend to the attribute string in the preGUI store. */ function makeNP(preGUI, guiEditFlag) { - if(!guiEditFlag) return nestedProperty; + if (!guiEditFlag) return nestedProperty; - return function(container, attr, prefix) { + return function (container, attr, prefix) { var np = nestedProperty(container, attr); var npSet = np.set; - np.set = function(val) { + np.set = function (val) { var fullAttr = (prefix || '') + attr; storeCurrent(fullAttr, np.get(), val, preGUI); npSet(val); @@ -1345,21 +1333,21 @@ function makeNP(preGUI, guiEditFlag) { } function storeCurrent(attr, val, newVal, preGUI) { - if(Array.isArray(val) || Array.isArray(newVal)) { + 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++) { + for (var i = 0; i < maxLen; i++) { storeCurrent(attr + '[' + i + ']', arrayVal[i], arrayNew[i], preGUI); } - } else if(Lib.isPlainObject(val) || Lib.isPlainObject(newVal)) { + } 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) { + for (var key in objBoth) { storeCurrent(attr + '.' + key, objVal[key], objNew[key], preGUI); } - } else if(preGUI[attr] === undefined) { + } else if (preGUI[attr] === undefined) { preGUI[attr] = undefinedToNull(val); } } @@ -1377,7 +1365,7 @@ function storeCurrent(attr, val, newVal, preGUI) { * @param {object} edits: the {attr: val} object as normally passed to `relayout` etc */ function _storeDirectGUIEdit(container, preGUI, edits) { - for(var attr in edits) { + for (var attr in edits) { var np = nestedProperty(container, attr); storeCurrent(attr, np.get(), edits[attr], preGUI); } @@ -1402,23 +1390,31 @@ function _restyle(gd, aobj, traces) { var axlist; // make a new empty vals array for undoit - function a0() { return traces.map(function() { return undefined; }); } + 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); + if (axlist.indexOf(axName) === -1) axlist.push(axName); } - function autorangeAttr(axName) { return 'LAYOUT' + axName + '.autorange'; } + function autorangeAttr(axName) { + return 'LAYOUT' + axName + '.autorange'; + } - function rangeAttr(axName) { return 'LAYOUT' + axName + '.range'; } + 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]; + 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. @@ -1430,15 +1426,17 @@ function _restyle(gd, aobj, traces) { // 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); }); + 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; + if (attr in aobj || helpers.hasParent(aobj, attr)) return; var extraparam; - if(attr.substr(0, 6) === 'LAYOUT') { + if (attr.substr(0, 6) === 'LAYOUT') { extraparam = layoutNP(gd.layout, attr.replace('LAYOUT', '')); } else { var tracei = traces[i]; @@ -1446,33 +1444,33 @@ function _restyle(gd, aobj, traces) { extraparam = makeNP(preGUI, guiEditFlag)(data[tracei], attr); } - if(!(attr in undoit)) { + if (!(attr in undoit)) { undoit[attr] = a0(); } - if(undoit[attr][i] === undefined) { + if (undoit[attr][i] === undefined) { undoit[attr][i] = undefinedToNull(extraparam.get()); } - if(val !== undefined) { + if (val !== undefined) { extraparam.set(val); } } function allBins(binAttr) { - return function(j) { + return function (j) { return fullData[j][binAttr]; }; } function arrayBins(binAttr) { - return function(vij, j) { + 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)) { + for (var ai in aobj) { + if (helpers.hasParent(aobj, ai)) { throw new Error('cannot set ' + ai + ' and a parent attribute simultaneously'); } @@ -1488,16 +1486,16 @@ function _restyle(gd, aobj, traces) { // 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') { + 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)); + 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') { + 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, @@ -1511,7 +1509,7 @@ function _restyle(gd, aobj, traces) { // set attribute in gd.data undoit[ai] = a0(); - for(i = 0; i < traces.length; i++) { + for (i = 0; i < traces.length; i++) { cont = data[traces[i]]; contFull = getFullTrace(traces[i]); var preGUI = fullLayout._tracePreGUI[contFull._fullInput.uid]; @@ -1519,24 +1517,24 @@ function _restyle(gd, aobj, traces) { oldVal = param.get(); newVal = Array.isArray(vi) ? vi[i % vi.length] : vi; - if(newVal === undefined) continue; + 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; + 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) { + 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 + } else if ( + (finalPart === 'thicknessmode' || finalPart === 'lenmode') && + oldVal !== newVal && + (newVal === 'fraction' || newVal === 'pixels') && + innerContFull ) { // changing colorbar size modes, // make the resulting size not change @@ -1546,23 +1544,25 @@ function _restyle(gd, aobj, traces) { var gs = fullLayout._size; var orient = innerContFull.orient; - var topOrBottom = (orient === 'top') || (orient === 'bottom'); - if(finalPart === 'thicknessmode') { + 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); + 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); + doextra(prefixDot + 'len', innerContFull.len * (newVal === 'fraction' ? 1 / lennorm : lennorm), i); } - } else if(ai === 'type' && ( - (newVal === 'pie') !== (oldVal === 'pie') || - (newVal === 'funnelarea') !== (oldVal === 'funnelarea') - )) { + } 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') { + if ((newVal === 'bar' || oldVal === 'bar') && cont.orientation === 'h') { labelsTo = 'y'; valuesTo = 'x'; } @@ -1570,54 +1570,50 @@ function _restyle(gd, aobj, traces) { 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()); + 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()); + } 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) { + 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') { + 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) { + var defaultOrientation = cont.x && !cont.y ? 'h' : 'v'; + if ((param.get() || defaultOrientation) === contFull.orientation) { continue; } - } else if(ai === 'orientationaxes') { + } else if (ai === 'orientationaxes') { // orientationaxes has no value, // it flips everything and the axes - cont.orientation = - {v: 'h', h: 'v'}[contFull.orientation]; + 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) { + } 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) { + if (valObject) { // must redo calcdata when restyling array values of arrayOk attributes // ... but no need to this for regl-based traces - if(valObject.arrayOk && + if ( + valObject.arrayOk && !Registry.traceIs(contFull, 'regl') && (Lib.isArrayOrTypedArray(newVal) || Lib.isArrayOrTypedArray(oldVal)) ) { @@ -1639,21 +1635,21 @@ function _restyle(gd, aobj, traces) { } // swap the data attributes of the relevant x and y axes? - if(['swapxyaxes', 'orientationaxes'].indexOf(ai) !== -1) { + if (['swapxyaxes', 'orientationaxes'].indexOf(ai) !== -1) { Axes.swap(gd, traces); } // swap hovermode if set to "compare x/y data" - if(ai === 'orientationaxes') { + if (ai === 'orientationaxes') { var hovermode = nestedProperty(gd.layout, 'hovermode'); var h = hovermode.get(); - if(h === 'x') { + if (h === 'x') { hovermode.set('y'); - } else if(h === 'y') { + } else if (h === 'y') { hovermode.set('x'); - } else if(h === 'x unified') { + } else if (h === 'x unified') { hovermode.set('y unified'); - } else if(h === 'y unified') { + } else if (h === 'y unified') { hovermode.set('x unified'); } } @@ -1664,12 +1660,12 @@ function _restyle(gd, aobj, traces) { // 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) { + if (['orientation', 'type'].indexOf(ai) !== -1) { axlist = []; - for(i = 0; i < traces.length; i++) { + for (i = 0; i < traces.length; i++) { var trace = data[traces[i]]; - if(Registry.traceIs(trace, 'cartesian')) { + if (Registry.traceIs(trace, 'cartesian')) { addToAxlist(trace.xaxis || 'x'); addToAxlist(trace.yaxis || 'y'); } @@ -1680,7 +1676,7 @@ function _restyle(gd, aobj, traces) { } } - if(flags.calc || flags.plot) { + if (flags.calc || flags.plot) { flags.fullReplot = true; } @@ -1718,22 +1714,22 @@ function relayout(gd, astr, val) { helpers.clearPromiseQueue(gd); var aobj = {}; - if(typeof astr === 'string') { + if (typeof astr === 'string') { aobj[astr] = val; - } else if(Lib.isPlainObject(astr)) { + } 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; + 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; + if (flags.calc) gd.calcdata = undefined; // fill in redraw sequence @@ -1741,37 +1737,30 @@ function relayout(gd, astr, val) { // something may have happened within relayout that we // need to wait for var seq = [Plots.previousPromises]; - if(flags.layoutReplot) { + if (flags.layoutReplot) { seq.push(subroutines.layoutReplot); - } else if(Object.keys(aobj).length) { + } 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); + 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 - ); + seq.push(Plots.rehover, Plots.redrag, Plots.reselect); - Queue.add(gd, - relayout, [gd, specs.undoit], - relayout, [gd, specs.redoit] - ); + 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); + if (!plotDone || !plotDone.then) plotDone = Promise.resolve(gd); - return plotDone.then(function() { + return plotDone.then(function () { gd.emit('plotly_relayout', specs.eventData); return gd; }); @@ -1782,29 +1771,29 @@ function relayout(gd, astr, val) { function axRangeSupplyDefaultsByPass(gd, flags, specs) { var fullLayout = gd._fullLayout; - if(!flags.axrange) return false; + if (!flags.axrange) return false; - for(var k in flags) { - if(k !== 'axrange' && flags[k]) return false; + for (var k in flags) { + if (k !== 'axrange' && flags[k]) return false; } var axIn, axOut; - var coerce = function(attr, dflt) { + 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) { + 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) { + 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(); @@ -1821,35 +1810,35 @@ 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'); - }; + 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, @@ -1880,14 +1869,14 @@ function _relayout(gd, 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++) { + 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 axisAttr = scene.indexOf('scene') !== -1 ? scene + '.' : ''; var newkey = keys[i].replace('allaxes', axisAttr + axes[j]._name); - if(!aobj[newkey]) aobj[newkey] = aobj[keys[i]]; + if (!aobj[newkey]) aobj[newkey] = aobj[keys[i]]; } delete aobj[keys[i]]; @@ -1907,20 +1896,22 @@ function _relayout(gd, aobj) { // 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); }); + 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; + if (attr in aobj || helpers.hasParent(aobj, attr)) return; var p = layoutNP(layout, attr); - if(!(attr in undoit)) { + if (!(attr in undoit)) { undoit[attr] = undefinedToNull(p.get()); } - if(val !== undefined) p.set(val); + if (val !== undefined) p.set(val); } // for constraint enforcement: keep track of all axes (as {id: name}) @@ -1936,8 +1927,8 @@ function _relayout(gd, aobj) { } // alter gd.layout - for(var ai in aobj) { - if(helpers.hasParent(aobj, ai)) { + for (var ai in aobj) { + if (helpers.hasParent(aobj, ai)) { throw new Error('cannot set ' + ai + ' and a parent attribute simultaneously'); } @@ -1946,7 +1937,7 @@ function _relayout(gd, aobj) { 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--; + while (pend > 0 && typeof p.parts[pend] !== 'string') pend--; // last property in chain (leaf node) var pleaf = p.parts[pend]; // leaf plus immediate parent @@ -1957,18 +1948,18 @@ function _relayout(gd, aobj) { var parentFull = nestedProperty(fullLayout, ptrunk).get(); var vOld = p.get(); - if(vi === undefined) continue; + 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); + 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) { + if (valObject && valObject.impliedEdits && vi !== null) { + for (var impliedKey in valObject.impliedEdits) { doextra(Lib.relativeAttr(ai, impliedKey), valObject.impliedEdits[impliedKey]); } } @@ -1978,8 +1969,8 @@ function _relayout(gd, aobj) { // // 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) { + 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 @@ -1989,25 +1980,25 @@ function _relayout(gd, aobj) { } else { fullLayout[ai] = gd._initialAutoSize[ai]; } - } else if(ai === 'autosize') { + } 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)) { + } 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)) { + } 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 (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)) { + } else if (pleafPlus.match(AX_DOMAIN_RE)) { nestedProperty(fullLayout, ptrunk + '._inputDomain').set(null); } @@ -2016,30 +2007,30 @@ function _relayout(gd, aobj) { // 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') { + 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) { + 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) { + } 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 (toLog) { // if both limits are negative, autorange - if(r0 <= 0 && r1 <= 0) { + 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; + 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); @@ -2047,15 +2038,16 @@ function _relayout(gd, aobj) { doextra(ptrunk + '.range[0]', Math.pow(10, r0)); doextra(ptrunk + '.range[1]', Math.pow(10, r1)); } - } else if(toLog) { + } 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]; + 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) && + if ( + Array.isArray(fullLayout._subplots.polar) && fullLayout._subplots.polar.length && fullLayout[p.parts[0]] && p.parts[1] === 'radialaxis' @@ -2074,14 +2066,14 @@ function _relayout(gd, aobj) { doextra(ptrunk + '.range', null); } nestedProperty(fullLayout, ptrunk + '._inputRange').set(null); - } else if(pleaf.match(AX_NAME_PATTERN)) { + } 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'; + if (!newType || newType === '-') newType = 'linear'; Registry.getComponentMethod('annotations', 'convertCoords')(gd, fullProp, newType, doextra); Registry.getComponentMethod('images', 'convertCoords')(gd, fullProp, newType, doextra); } @@ -2093,19 +2085,19 @@ function _relayout(gd, aobj) { // and order-independence for add/remove/edit all together in // one relayout call var containerArrayMatch = manageArrays.containerArrayMatch(ai); - if(containerArrayMatch) { + if (containerArrayMatch) { arrayStr = containerArrayMatch.array; i = containerArrayMatch.index; var propStr = containerArrayMatch.property; - var updateValObject = valObject || {editType: 'calc'}; + var updateValObject = valObject || { editType: 'calc' }; - if(i !== '' && propStr === '') { + 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)) { + if (manageArrays.isAddVal(vi)) { undoit[ai] = null; - } else if(manageArrays.isRemoveVal(vi)) { + } else if (manageArrays.isRemoveVal(vi)) { undoit[ai] = (nestedProperty(layout, arrayStr).get() || [])[i]; } else { Lib.warn('unrecognized full object value', aobj); @@ -2114,33 +2106,35 @@ function _relayout(gd, aobj) { editTypes.update(flags, updateValObject); // prepare the edits object we'll send to applyContainerArrayChanges - if(!arrayEdits[arrayStr]) arrayEdits[arrayStr] = {}; + if (!arrayEdits[arrayStr]) arrayEdits[arrayStr] = {}; var objEdits = arrayEdits[arrayStr][i]; - if(!objEdits) objEdits = arrayEdits[arrayStr][i] = {}; + if (!objEdits) objEdits = arrayEdits[arrayStr][i] = {}; objEdits[propStr] = vi; delete aobj[ai]; - } else if(pleaf === 'reverse') { + } else if (pleaf === 'reverse') { // handle axis reversal explicitly, as there's no 'reverse' attribute - if(parentIn.range) parentIn.range.reverse(); + if (parentIn.range) parentIn.range.reverse(); else { doextra(ptrunk + '.autorange', true); parentIn.range = [1, 0]; } - if(parentFull.autorange) flags.calc = true; + if (parentFull.autorange) flags.calc = true; else flags.plot = true; } else { - if(ai === 'dragmode' && ((vi === false && vOld !== false) || (vi !== false && vOld === false))) { + 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' && + } else if ( + fullLayout._has('scatter-like') && + fullLayout._has('regl') && + ai === 'dragmode' && (vi === 'lasso' || vi === 'select') && - !(vOld === 'lasso' || vOld === 'select')) + !(vOld === 'lasso' || vOld === 'select') ) { flags.plot = true; - } else if(valObject) editTypes.update(flags, valObject); + } else if (valObject) editTypes.update(flags, valObject); else flags.calc = true; p.set(vi); @@ -2148,25 +2142,30 @@ function _relayout(gd, aobj) { } // 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; + 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) { + for (var axId in rangesAltered) { ax = Axes.getFromId(gd, axId); var group = ax && ax._constraintGroup; - if(group) { + 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]) { + for (var groupAxId in group) { + if (!rangesAltered[groupAxId]) { Axes.getFromId(gd, groupAxId)._constraintShrinkable = true; } } @@ -2177,18 +2176,18 @@ function _relayout(gd, aobj) { // 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; + 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) { + for (i = 0; i < shapes.length; i++) { + if (shapes[i].showlegend) { flags.calc = true; break; } } - if(flags.plot || flags.calc) { + if (flags.plot || flags.calc) { flags.layoutReplot = true; } @@ -2215,9 +2214,9 @@ function updateAutosize(gd) { var oldHeight = fullLayout.height; // calculate autosizing - if(gd.layout.autosize) Plots.plotAutoSize(gd, gd.layout, fullLayout); + if (gd.layout.autosize) Plots.plotAutoSize(gd, gd.layout, fullLayout); - return (fullLayout.width !== oldWidth) || (fullLayout.height !== oldHeight); + return fullLayout.width !== oldWidth || fullLayout.height !== oldHeight; } /** @@ -2239,11 +2238,11 @@ function update(gd, traceUpdate, layoutUpdate, _traces) { gd = Lib.getGraphDiv(gd); helpers.clearPromiseQueue(gd); - if(!Lib.isPlainObject(traceUpdate)) traceUpdate = {}; - if(!Lib.isPlainObject(layoutUpdate)) layoutUpdate = {}; + 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; + if (Object.keys(traceUpdate).length) gd.changed = true; + if (Object.keys(layoutUpdate).length) gd.changed = true; var traces = helpers.coerceTraceIndices(gd, _traces); @@ -2254,49 +2253,47 @@ function update(gd, traceUpdate, layoutUpdate, _traces) { 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); + 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) { + if (relayoutFlags.layoutReplot) { // N.B. works fine when both // relayoutFlags.layoutReplot and restyleFlags.fullReplot are true seq.push(subroutines.layoutReplot); - } else if(restyleFlags.fullReplot) { + } 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); + 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 - ); + 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] - ); + 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); + if (!plotDone || !plotDone.then) plotDone = Promise.resolve(gd); - return plotDone.then(function() { + return plotDone.then(function () { gd.emit('plotly_update', { data: restyleSpecs.eventData, layout: relayoutSpecs.eventData @@ -2324,35 +2321,35 @@ function guiEdit(func) { // 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)/}, + { 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'} + { 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'}, + { 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 + { 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` @@ -2360,19 +2357,19 @@ var traceUIControlPatterns = [ // Also applies to axis titles up in the layout section // "name" also includes transform.styles - {pattern: /(^|value\.)name$/}, + { pattern: /(^|value\.)name$/ }, // including nested colorbar attributes (ie marker.colorbar) - {pattern: /colorbar\.title\.text$/}, - {pattern: /colorbar\.(x|y)$/, attr: 'editrevision'} + { pattern: /colorbar\.title\.text$/ }, + { pattern: /colorbar\.(x|y)$/, attr: 'editrevision' } ]; function findUIPattern(key, patternSpecs) { - for(var i = 0; i < patternSpecs.length; i++) { + for (var i = 0; i < patternSpecs.length; i++) { var spec = patternSpecs[i]; var match = key.match(spec.pattern); - if(match) { + if (match) { var head = match[1] || ''; - return {head: head, tail: key.substr(head.length + 1), attr: spec.attr}; + return { head: head, tail: key.substr(head.length + 1), attr: spec.attr }; } } } @@ -2382,42 +2379,42 @@ function findUIPattern(key, patternSpecs) { // falsy values are returned. function getNewRev(revAttr, container) { var newRev = nestedProperty(container, revAttr).get(); - if(newRev !== undefined) return newRev; + if (newRev !== undefined) return newRev; var parts = revAttr.split('.'); parts.pop(); - while(parts.length > 1) { + while (parts.length > 1) { parts.pop(); newRev = nestedProperty(container, parts.join('.') + '.uirevision').get(); - if(newRev !== undefined) return newRev; + 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; + 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; + 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; + 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) { + if (v1IsObj || v1IsArray) { return ( - (v1IsObj && Lib.isPlainObject(v2)) || - (v1IsArray && Array.isArray(v2)) - ) && JSON.stringify(v1) === JSON.stringify(v2); + ((v1IsObj && Lib.isPlainObject(v2)) || (v1IsArray && Array.isArray(v2))) && + JSON.stringify(v1) === JSON.stringify(v2) + ); } return v1 === v2; } @@ -2428,46 +2425,45 @@ function applyUIRevisions(data, layout, oldFullData, oldFullLayout) { var bothInheritAutorange = []; var newAutorangeIn = {}; var newRangeAccepted = {}; - for(key in layoutPreGUI) { + for (key in layoutPreGUI) { match = findUIPattern(key, layoutUIControlPatterns); - if(match) { + if (match) { head = match.head; tail = match.tail; - revAttr = match.attr || (head + '.uirevision'); + revAttr = match.attr || head + '.uirevision'; oldRev = nestedProperty(oldFullLayout, revAttr).get(); newRev = oldRev && getNewRev(revAttr, layout); - if(newRev && (newRev === oldRev)) { + if (newRev && newRev === oldRev) { preGUIVal = layoutPreGUI[key]; - if(preGUIVal === null) preGUIVal = undefined; + if (preGUIVal === null) preGUIVal = undefined; newNP = nestedProperty(layout, key); newVal = newNP.get(); - if(valsMatch(newVal, preGUIVal)) { - if(newVal === undefined && tail === 'autorange') { + 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[') { + } 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)) { + 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)) { + if (!(head in newAutorangeIn)) { var newContainer = nestedProperty(layout, head).get(); - newAutorangeIn[head] = newContainer && ( - newContainer.autorange || - (newContainer.autorange !== false && ( - !newContainer.range || newContainer.range.length !== 2) - ) - ); + newAutorangeIn[head] = + newContainer && + (newContainer.autorange || + (newContainer.autorange !== false && + (!newContainer.range || newContainer.range.length !== 2))); } - if(newAutorangeIn[head]) { + if (newAutorangeIn[head]) { newNP.set(undefinedToNull(nestedProperty(oldFullLayout, key).get())); continue; } @@ -2482,7 +2478,7 @@ function applyUIRevisions(data, layout, oldFullData, oldFullLayout) { // so remove it from _preGUI for next time. delete layoutPreGUI[key]; - if(match && match.tail.substr(0, 6) === 'range[') { + if (match && match.tail.substr(0, 6) === 'range[') { newRangeAccepted[match.head] = 1; } } @@ -2491,27 +2487,27 @@ function applyUIRevisions(data, layout, oldFullData, oldFullLayout) { // 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++) { + for (var i = 0; i < bothInheritAutorange.length; i++) { var axAttr = bothInheritAutorange[i]; - if(newRangeAccepted[axAttr]) { + if (newRangeAccepted[axAttr]) { var newAx = nestedProperty(layout, axAttr).get(); - if(newAx) delete newAx.autorange; + 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) { + for (var uid in allTracePreGUI) { var tracePreGUI = allTracePreGUI[uid]; var newTrace = null; var fullInput; - for(key in tracePreGUI) { + 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) { + if (!newTrace) { var fulli = getFullTraceIndexFromUid(uid, oldFullData); - if(fulli < 0) { + 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]; @@ -2521,7 +2517,7 @@ function applyUIRevisions(data, layout, oldFullData, oldFullLayout) { fullInput = fullTrace._fullInput; var newTracei = getTraceIndexFromUid(uid, data, fullInput.index); - if(newTracei < 0) { + if (newTracei < 0) { // No match in new data delete allTracePreGUI[uid]; break; @@ -2530,23 +2526,23 @@ function applyUIRevisions(data, layout, oldFullData, oldFullLayout) { } match = findUIPattern(key, traceUIControlPatterns); - if(match) { - if(match.attr) { + 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 === undefined) newRev = layout.uirevision; } - if(newRev && newRev === oldRev) { + if (newRev && newRev === oldRev) { preGUIVal = tracePreGUI[key]; - if(preGUIVal === null) preGUIVal = undefined; + if (preGUIVal === null) preGUIVal = undefined; newNP = nestedProperty(newTrace, key); newVal = newNP.get(); - if(valsMatch(newVal, preGUIVal)) { + if (valsMatch(newVal, preGUIVal)) { newNP.set(undefinedToNull(nestedProperty(fullInput, key).get())); continue; } @@ -2585,7 +2581,9 @@ function applyUIRevisions(data, layout, oldFullData, oldFullLayout) { function react(gd, data, layout, config) { var frames, plotDone; - function addFrames() { return exports.addFrames(gd, frames); } + function addFrames() { + return exports.addFrames(gd, frames); + } gd = Lib.getGraphDiv(gd); helpers.clearPromiseQueue(gd); @@ -2594,10 +2592,10 @@ function react(gd, data, layout, config) { var oldFullLayout = gd._fullLayout; // you can use this as the initial draw as well as to update - if(!Lib.isPlotDiv(gd) || !oldFullData || !oldFullLayout) { + if (!Lib.isPlotDiv(gd) || !oldFullData || !oldFullLayout) { plotDone = exports.newPlot(gd, data, layout, config); } else { - if(Lib.isPlainObject(data)) { + if (Lib.isPlainObject(data)) { var obj = data; data = obj.data; layout = obj.layout; @@ -2608,25 +2606,24 @@ function react(gd, data, layout, config) { 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); + if (config) { + const oldConfig = Lib.extendDeepAll({}, gd._context); gd._context = undefined; setPlotContext(gd, config); - configChanged = diffConfig(oldConfig, gd._context); + configChanged = helpers.hasCollectionChanged(oldConfig, gd._context); } - if(configChanged) { + if (configChanged) { // Save event listeners as newPlot will remove them - const eventListeners = gd._ev.eventNames().map(name => [name, gd._ev.listeners(name)]); - plotDone = exports.newPlot(gd, data, layout, config) - .then(() => { - for (const [name, callbacks] of eventListeners) { - callbacks.forEach((cb) => gd.on(name, cb)); - } - - // Call react in case transition should have occurred along with config change - return exports.react(gd, data, layout, config) - }); + const eventListeners = gd._ev.eventNames().map((name) => [name, gd._ev.listeners(name)]); + plotDone = exports.newPlot(gd, data, layout, config).then(() => { + for (const [name, callbacks] of eventListeners) { + callbacks.forEach((cb) => gd.on(name, cb)); + } + + // Call react in case transition should have occurred along with config change + return exports.react(gd, data, layout, config); + }); } else { gd.data = data || []; helpers.cleanData(gd.data); @@ -2638,7 +2635,7 @@ function react(gd, data, layout, config) { // "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}); + Plots.supplyDefaults(gd, { skipUpdateCalc: true }); var newFullData = gd._fullData; var newFullLayout = gd._fullLayout; @@ -2658,21 +2655,21 @@ function react(gd, data, layout, config) { // fullLayout[ai] = gd._initialAutoSize[ai]; // } - if(updateAutosize(gd)) relayoutFlags.layoutReplot = true; + if (updateAutosize(gd)) relayoutFlags.layoutReplot = true; // clear calcdata and empty categories if required - if(restyleFlags.calc || relayoutFlags.calc) { + if (restyleFlags.calc || relayoutFlags.calc) { gd.calcdata = undefined; var allNames = Object.getOwnPropertyNames(newFullLayout); - for(var q = 0; q < allNames.length; q++) { + for (var q = 0; q < allNames.length; q++) { var name = allNames[q]; var start = name.substring(0, 5); - if(start === 'xaxis' || start === 'yaxis') { + if (start === 'xaxis' || start === 'yaxis') { var emptyCategories = newFullLayout[name]._emptyCategories; - if(emptyCategories) emptyCategories(); + if (emptyCategories) emptyCategories(); } } - // otherwise do the calcdata updates and calcTransform array remaps that we skipped earlier + // otherwise do the calcdata updates and calcTransform array remaps that we skipped earlier } else { Plots.supplyDefaultsUpdateCalc(gd.calcdata, newFullData); } @@ -2683,7 +2680,7 @@ function react(gd, data, layout, config) { // fill in redraw sequence var seq = []; - if(frames) { + if (frames) { gd._transitionData = {}; Plots.createTransitionData(gd); seq.push(addFrames); @@ -2693,30 +2690,30 @@ function react(gd, data, layout, config) { // 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 && (restyleFlags.anim || relayoutFlags.anim)) { - if(relayoutFlags.ticks) seq.push(subroutines.doTicksRelayout); + if (newFullLayout.transition && (restyleFlags.anim || relayoutFlags.anim)) { + if (relayoutFlags.ticks) seq.push(subroutines.doTicksRelayout); Plots.doCalcdata(gd); subroutines.doAutoRangeAndConstraints(gd); - seq.push(function() { + seq.push(function () { return Plots.transitionFromReact(gd, restyleFlags, relayoutFlags, oldFullLayout); }); - } else if(restyleFlags.fullReplot || relayoutFlags.layoutReplot) { + } else if (restyleFlags.fullReplot || relayoutFlags.layoutReplot) { gd._fullLayout._skipDefaults = true; seq.push(exports._doPlot); } else { - for(var componentType in relayoutFlags.arrays) { + for (var componentType in relayoutFlags.arrays) { var indices = relayoutFlags.arrays[componentType]; - if(indices.length) { + if (indices.length) { var drawOne = Registry.getComponentMethod(componentType, 'drawOne'); - if(drawOne !== Lib.noop) { - for(var i = 0; i < indices.length; i++) { + 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) { + if (draw === Lib.noop) { throw new Error('cannot draw components: ' + componentType); } draw(gd); @@ -2725,25 +2722,21 @@ function react(gd, data, layout, config) { } 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); + 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 - ); + seq.push(Plots.rehover, Plots.redrag, Plots.reselect); plotDone = Lib.syncOrAsync(seq, gd); - if(!plotDone || !plotDone.then) plotDone = Promise.resolve(gd); + if (!plotDone || !plotDone.then) plotDone = Promise.resolve(gd); } } @@ -2757,7 +2750,7 @@ function react(gd, data, layout, config) { function diffData(gd, oldFullData, newFullData, immutable, transition, newDataRevision) { var sameTraceLength = oldFullData.length === newFullData.length; - if(!transition && !sameTraceLength) { + if (!transition && !sameTraceLength) { return { fullReplot: true, calc: true @@ -2773,7 +2766,7 @@ function diffData(gd, oldFullData, newFullData, immutable, transition, newDataRe function getTraceValObject(parts) { var out = PlotSchema.getTraceValObject(trace, parts); - if(!trace._module.animatable && out.anim) { + if (!trace._module.animatable && out.anim) { out.anim = false; } return out; @@ -2790,22 +2783,22 @@ function diffData(gd, oldFullData, newFullData, immutable, transition, newDataRe var seenUIDs = {}; - for(i = 0; i < oldFullData.length; i++) { - if(newFullData[i]) { + for (i = 0; i < oldFullData.length; i++) { + if (newFullData[i]) { trace = newFullData[i]._fullInput; - if(seenUIDs[trace.uid]) continue; + if (seenUIDs[trace.uid]) continue; seenUIDs[trace.uid] = 1; getDiffFlags(oldFullData[i]._fullInput, trace, [], diffOpts); } } - if(flags.calc || flags.plot) { + if (flags.calc || flags.plot) { flags.fullReplot = true; } - if(transition && flags.nChanges && flags.nChangesAnim) { - flags.anim = (flags.nChanges === flags.nChangesAnim) && sameTraceLength ? 'all' : 'some'; + if (transition && flags.nChanges && flags.nChangesAnim) { + flags.anim = flags.nChanges === flags.nChangesAnim && sameTraceLength ? 'all' : 'some'; } return flags; @@ -2858,11 +2851,11 @@ function diffLayout(gd, oldFullLayout, newFullLayout, immutable, transition) { getDiffFlags(oldFullLayout, newFullLayout, [], diffOpts); - if(flags.plot || flags.calc) { + if (flags.plot || flags.calc) { flags.layoutReplot = true; } - if(transition && flags.nChanges && flags.nChangesAnim) { + if (transition && flags.nChanges && flags.nChangesAnim) { flags.anim = flags.nChanges === flags.nChangesAnim ? 'all' : 'some'; } @@ -2880,28 +2873,28 @@ function getDiffFlags(oldContainer, newContainer, outerparts, opts) { function changed() { var editType = valObject.editType; - if(inArray && editType.indexOf('arraydraw') !== -1) { + if (inArray && editType.indexOf('arraydraw') !== -1) { Lib.pushUnique(flags.arrays[inArray], arrayIndex); return; } editTypes.update(flags, valObject); - if(editType !== 'none') { + if (editType !== 'none') { flags.nChanges++; } // track animatable changes - if(opts.transition && valObject.anim) { + if (opts.transition && valObject.anim) { flags.nChangesAnim++; } // track cartesian axes with altered ranges - if(AX_RANGE_RE.test(astr) || AX_AUTORANGE_RE.test(astr)) { + if (AX_RANGE_RE.test(astr) || AX_AUTORANGE_RE.test(astr)) { flags.rangesAltered[outerparts[0]] = 1; } // track datarevision changes - if(key === 'datarevision') { + if (key === 'datarevision') { flags.newDataRevision = 1; } } @@ -2910,35 +2903,35 @@ function getDiffFlags(oldContainer, newContainer, outerparts, opts) { return valObject.valType === 'data_array' || valObject.arrayOk; } - for(key in oldContainer) { + for (key in oldContainer) { // short-circuit based on previous calls or previous keys that already maximized the pathway - if(flags.calc && !opts.transition) return; + 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; + 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') { + if ((key === 'tick0' || key === 'dtick') && outerparts[0] !== 'geo') { var tickMode = newContainer.tickmode; - if(tickMode === 'auto' || tickMode === 'array' || !tickMode) continue; + 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; + 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) continue; - if(valObject._compareAsJSON && JSON.stringify(oldVal) === JSON.stringify(newVal)) continue; + if (valObject._compareAsJSON && JSON.stringify(oldVal) === JSON.stringify(newVal)) continue; var valType = valObject.valType; var i; @@ -2949,25 +2942,25 @@ function getDiffFlags(oldContainer, newContainer, outerparts, opts) { // hack for traces that modify the data in supplyDefaults, like // converting 1D to 2D arrays, which will always create new objects - if(wasArray && nowArray) { + if (wasArray && nowArray) { var inputKey = '_input_' + key; var oldValIn = oldContainer[inputKey]; var newValIn = newContainer[inputKey]; - if(Array.isArray(oldValIn) && oldValIn === newValIn) continue; + if (Array.isArray(oldValIn) && oldValIn === newValIn) continue; } - if(newVal === undefined) { - if(canBeDataArray && wasArray) flags.calc = true; + if (newVal === undefined) { + if (canBeDataArray && wasArray) flags.calc = true; else changed(); - } else if(valObject._isLinkedToArray) { + } else if (valObject._isLinkedToArray) { var arrayEditIndices = []; var extraIndices = false; - if(!inArray) flags.arrays[key] = arrayEditIndices; + 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') { + if (minLen !== maxLen) { + if (valObject.editType === 'arraydraw') { extraIndices = true; } else { changed(); @@ -2975,43 +2968,47 @@ function getDiffFlags(oldContainer, newContainer, outerparts, opts) { } } - for(i = 0; i < minLen; i++) { - getDiffFlags(oldVal[i], newVal[i], parts.concat(i), + 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)); + 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++) { + if (extraIndices) { + for (i = minLen; i < maxLen; i++) { arrayEditIndices.push(i); } } - } else if(!valType && Lib.isPlainObject(oldVal)) { + } else if (!valType && Lib.isPlainObject(oldVal)) { getDiffFlags(oldVal, newVal, parts, opts); - } else if(canBeDataArray) { - if(wasArray && nowArray) { + } 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) { + if (immutable) { flags.calc = true; } // look for animatable attributes when the data changed - if(immutable || opts.newDataRevision) { + if (immutable || opts.newDataRevision) { changed(); } - } else if(wasArray !== nowArray) { + } else if (wasArray !== nowArray) { flags.calc = true; } else changed(); - } else if(wasArray && nowArray) { + } 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)) { + if (oldVal.length !== newVal.length || String(oldVal) !== String(newVal)) { changed(); } } else { @@ -3019,11 +3016,11 @@ function getDiffFlags(oldContainer, newContainer, outerparts, opts) { } } - for(key in newContainer) { - if(!(key in oldContainer || key.charAt(0) === '_' || typeof newContainer[key] === 'function')) { + 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])) { + if (valObjectCanBeDataArray(valObject) && Array.isArray(newContainer[key])) { flags.calc = true; return; } else changed(); @@ -3031,43 +3028,6 @@ function getDiffFlags(oldContainer, newContainer, outerparts, opts) { } } -/* - * 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 * @@ -3098,11 +3058,13 @@ function diffConfig(oldConfig, newConfig) { function animate(gd, frameOrGroupNameOrFrameList, animationOpts) { gd = Lib.getGraphDiv(gd); - if(!Lib.isPlotDiv(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/' + '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/' ); } @@ -3110,7 +3072,7 @@ function animate(gd, frameOrGroupNameOrFrameList, animationOpts) { // 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) { + if (!trans._frameQueue) { trans._frameQueue = []; } @@ -3122,13 +3084,13 @@ function animate(gd, frameOrGroupNameOrFrameList, animationOpts) { // *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) { + if (trans._frameWaitingCnt === undefined) { trans._frameWaitingCnt = 0; } function getTransitionOpts(i) { - if(Array.isArray(transitionOpts)) { - if(i >= transitionOpts.length) { + if (Array.isArray(transitionOpts)) { + if (i >= transitionOpts.length) { return transitionOpts[0]; } else { return transitionOpts[i]; @@ -3139,8 +3101,8 @@ function animate(gd, frameOrGroupNameOrFrameList, animationOpts) { } function getFrameOpts(i) { - if(Array.isArray(frameOpts)) { - if(i >= frameOpts.length) { + if (Array.isArray(frameOpts)) { + if (i >= frameOpts.length) { return frameOpts[0]; } else { return frameOpts[i]; @@ -3157,22 +3119,22 @@ function animate(gd, frameOrGroupNameOrFrameList, animationOpts) { // or vice versa. function callbackOnNthTime(cb, n) { var cnt = 0; - return function() { - if(cb && ++cnt === n) { + return function () { + if (cb && ++cnt === n) { return cb(); } }; } - return new Promise(function(resolve, reject) { + return new Promise(function (resolve, reject) { function discardExistingFrames() { - if(trans._frameQueue.length === 0) { + if (trans._frameQueue.length === 0) { return; } - while(trans._frameQueue.length) { + while (trans._frameQueue.length) { var next = trans._frameQueue.pop(); - if(next.onInterrupt) { + if (next.onInterrupt) { next.onInterrupt(); } } @@ -3181,12 +3143,12 @@ function animate(gd, frameOrGroupNameOrFrameList, animationOpts) { } function queueFrames(frameList) { - if(frameList.length === 0) return; + if (frameList.length === 0) return; - for(var i = 0; i < frameList.length; i++) { + for (var i = 0; i < frameList.length; i++) { var computedFrame; - if(frameList[i].type === 'byname') { + if (frameList[i].type === 'byname') { // If it's a named frame, compute it: computedFrame = Plots.computeFrame(gd, frameList[i].name); } else { @@ -3206,9 +3168,9 @@ function animate(gd, frameOrGroupNameOrFrameList, animationOpts) { frame: computedFrame, name: frameList[i].name, frameOpts: frameOpts, - transitionOpts: transitionOpts, + transitionOpts: transitionOpts }; - if(i === frameList.length - 1) { + 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) @@ -3225,7 +3187,7 @@ function animate(gd, frameOrGroupNameOrFrameList, animationOpts) { // 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') { + if (animationOpts.mode === 'immediate') { trans._lastFrameAt = -Infinity; } @@ -3234,7 +3196,7 @@ function animate(gd, frameOrGroupNameOrFrameList, animationOpts) { // 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) { + if (!trans._animationRaf) { beginAnimationLoop(); } } @@ -3248,15 +3210,15 @@ function animate(gd, frameOrGroupNameOrFrameList, animationOpts) { } function nextFrame() { - if(trans._currentFrame && trans._currentFrame.onComplete) { + 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(); + var newFrame = (trans._currentFrame = trans._frameQueue.shift()); - if(newFrame) { + 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: @@ -3269,14 +3231,15 @@ function animate(gd, frameOrGroupNameOrFrameList, animationOpts) { // 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, + Plots.transition( + gd, newFrame.frame.data, newFrame.frame.layout, helpers.coerceTraceIndices(gd, newFrame.frame.traces), newFrame.frameOpts, newFrame.transitionOpts - ).then(function() { - if(newFrame.onComplete) { + ).then(function () { + if (newFrame.onComplete) { newFrame.onComplete(); } }); @@ -3286,7 +3249,7 @@ function animate(gd, frameOrGroupNameOrFrameList, animationOpts) { frame: newFrame.frame, animation: { frame: newFrame.frameOpts, - transition: newFrame.transitionOpts, + transition: newFrame.transitionOpts } }); } else { @@ -3305,13 +3268,13 @@ function animate(gd, frameOrGroupNameOrFrameList, animationOpts) { trans._runningTransitions = 0; trans._currentFrame = null; - var doFrame = function() { + 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) { + if (Date.now() - trans._lastFrameAt > trans._timeToNext) { nextFrame(); } }; @@ -3323,8 +3286,8 @@ function animate(gd, frameOrGroupNameOrFrameList, animationOpts) { // items with the particular frame. var configCounter = 0; function setTransitionConfig(frame) { - if(Array.isArray(transitionOpts)) { - if(configCounter >= transitionOpts.length) { + if (Array.isArray(transitionOpts)) { + if (configCounter >= transitionOpts.length) { frame.transitionOpts = transitionOpts[configCounter]; } else { frame.transitionOpts = transitionOpts[0]; @@ -3343,40 +3306,40 @@ function animate(gd, frameOrGroupNameOrFrameList, animationOpts) { var isFrameArray = Array.isArray(frameOrGroupNameOrFrameList); var isSingleFrame = !allFrames && !isFrameArray && Lib.isPlainObject(frameOrGroupNameOrFrameList); - if(isSingleFrame) { + 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) { + } 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++) { + for (i = 0; i < trans._frames.length; i++) { frame = trans._frames[i]; - if(!frame) continue; + if (!frame) continue; - if(allFrames || String(frame.group) === String(frameOrGroupNameOrFrameList)) { + if (allFrames || String(frame.group) === String(frameOrGroupNameOrFrameList)) { frameList.push({ type: 'byname', name: String(frame.name), - data: setTransitionConfig({name: frame.name}) + data: setTransitionConfig({ name: frame.name }) }); } } - } else if(isFrameArray) { - for(i = 0; i < frameOrGroupNameOrFrameList.length; i++) { + } else if (isFrameArray) { + for (i = 0; i < frameOrGroupNameOrFrameList.length; i++) { var frameOrName = frameOrGroupNameOrFrameList[i]; - if(['number', 'string'].indexOf(typeof frameOrName) !== -1) { + 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}) + data: setTransitionConfig({ name: frameOrName }) }); - } else if(Lib.isPlainObject(frameOrName)) { + } else if (Lib.isPlainObject(frameOrName)) { frameList.push({ type: 'object', data: setTransitionConfig(Lib.extendFlat({}, frameOrName)) @@ -3386,9 +3349,9 @@ function animate(gd, frameOrGroupNameOrFrameList, animationOpts) { } // Verify that all of these frames actually exist; return and reject if not: - for(i = 0; i < frameList.length; i++) { + for (i = 0; i < frameList.length; i++) { frame = frameList[i]; - if(frame.type === 'byname' && !trans._frameHash[frame.data.name]) { + if (frame.type === 'byname' && !trans._frameHash[frame.data.name]) { Lib.warn('animate failure: frame not found: "' + frame.data.name + '"'); reject(); return; @@ -3397,30 +3360,30 @@ function animate(gd, frameOrGroupNameOrFrameList, animationOpts) { // 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) { + if (['next', 'immediate'].indexOf(animationOpts.mode) !== -1) { discardExistingFrames(); } - if(animationOpts.direction === 'reverse') { + if (animationOpts.direction === 'reverse') { frameList.reverse(); } var currentFrame = gd._fullLayout._currentFrame; - if(currentFrame && animationOpts.fromcurrent) { + if (currentFrame && animationOpts.fromcurrent) { var idx = -1; - for(i = 0; i < frameList.length; i++) { + for (i = 0; i < frameList.length; i++) { frame = frameList[i]; - if(frame.type === 'byname' && frame.name === currentFrame) { + if (frame.type === 'byname' && frame.name === currentFrame) { idx = i; break; } } - if(idx > 0 && idx < frameList.length - 1) { + if (idx > 0 && idx < frameList.length - 1) { var filteredFrameList = []; - for(i = 0; i < frameList.length; i++) { + for (i = 0; i < frameList.length; i++) { frame = frameList[i]; - if(frameList[i].type !== 'byname' || i > idx) { + if (frameList[i].type !== 'byname' || i > idx) { filteredFrameList.push(frame); } } @@ -3428,7 +3391,7 @@ function animate(gd, frameOrGroupNameOrFrameList, animationOpts) { } } - if(frameList.length > 0) { + if (frameList.length > 0) { queueFrames(frameList); } else { // This is the case where there were simply no frames. It's a little strange @@ -3461,15 +3424,17 @@ function animate(gd, frameOrGroupNameOrFrameList, animationOpts) { function addFrames(gd, frameList, indices) { gd = Lib.getGraphDiv(gd); - if(frameList === null || frameList === undefined) { + if (frameList === null || frameList === undefined) { return Promise.resolve(); } - if(!Lib.isPlotDiv(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 adding frames. For more details, see ' + - 'https://plotly.com/javascript/animations/' + '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/' ); } @@ -3477,8 +3442,7 @@ function addFrames(gd, frameList, indices) { var _frames = gd._transitionData._frames; var _frameHash = gd._transitionData._frameHash; - - if(!Array.isArray(frameList)) { + if (!Array.isArray(frameList)) { throw new Error('addFrames failure: frameList must be an Array of frame definitions' + frameList); } @@ -3491,8 +3455,8 @@ function addFrames(gd, frameList, indices) { var insertions = []; var _frameHashLocal = {}; - for(i = frameList.length - 1; i >= 0; i--) { - if(!Lib.isPlainObject(frameList[i])) continue; + 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. @@ -3501,34 +3465,46 @@ function addFrames(gd, frameList, indices) { var newName = frameList[i].name; var collisionPresent = _frameHash[name] || _frameHashLocal[name]; - if(name && newName && typeof newName === 'number' && collisionPresent && numericNameWarningCount < numericNameWarningCountLimit) { + 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.'); + 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.'); + 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}; + _frameHashLocal[lookupName] = { name: lookupName }; insertions.push({ frame: Plots.supplyFrameDefaults(frameList[i]), - index: (indices && indices[i] !== undefined && indices[i] !== null) ? indices[i] : bigIndex + 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; + insertions.sort(function (a, b) { + if (a.index > b.index) return -1; + if (a.index < b.index) return 1; return 0; }); @@ -3536,33 +3512,35 @@ function addFrames(gd, frameList, indices) { var revops = []; var frameCount = _frames.length; - for(i = insertions.length - 1; i >= 0; i--) { + 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 (typeof frame.name === 'number') { + Lib.warn( + 'Warning: addFrames accepts frames with numeric names, but the numbers are' + + 'implicitly cast to strings' + ); } - if(!frame.name) { + 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++)]); + while (_frameHash[(frame.name = 'frame ' + gd._transitionData._counter++)]); } - if(_frameHash[frame.name]) { + 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; + 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]}); + 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}); + ops.push({ type: 'insert', index: idx, value: frame }); + revops.unshift({ type: 'delete', index: idx }); frameCount++; } } @@ -3572,7 +3550,7 @@ function addFrames(gd, frameList, indices) { var undoArgs = [gd, revops]; var redoArgs = [gd, ops]; - if(Queue) Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs); + if (Queue) Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs); return Plots.modifyFrames(gd, ops); } @@ -3589,7 +3567,7 @@ function addFrames(gd, frameList, indices) { function deleteFrames(gd, frameList) { gd = Lib.getGraphDiv(gd); - if(!Lib.isPlotDiv(gd)) { + if (!Lib.isPlotDiv(gd)) { throw new Error('This element is not a Plotly plot: ' + gd); } @@ -3598,9 +3576,9 @@ function deleteFrames(gd, frameList) { var ops = []; var revops = []; - if(!frameList) { + if (!frameList) { frameList = []; - for(i = 0; i < _frames.length; i++) { + for (i = 0; i < _frames.length; i++) { frameList.push(i); } } @@ -3608,10 +3586,10 @@ function deleteFrames(gd, frameList) { frameList = frameList.slice(); frameList.sort(); - for(i = frameList.length - 1; i >= 0; i--) { + 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]}); + ops.push({ type: 'delete', index: idx }); + revops.unshift({ type: 'insert', index: idx, value: _frames[idx] }); } var undoFunc = Plots.modifyFrames; @@ -3619,7 +3597,7 @@ function deleteFrames(gd, frameList) { var undoArgs = [gd, revops]; var redoArgs = [gd, ops]; - if(Queue) Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs); + if (Queue) Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs); return Plots.modifyFrames(gd, ops); } @@ -3646,7 +3624,7 @@ function purge(gd) { Events.purge(gd); // remove plot container - if(fullLayout._container) fullLayout._container.remove(); + if (fullLayout._container) fullLayout._container.remove(); // in contrast to _doPlots.purge which does NOT clear _context! delete gd._context; @@ -3659,9 +3637,9 @@ function calcInverseTransform(gd) { var fullLayout = gd._fullLayout; var newBBox = gd.getBoundingClientRect(); - if(Lib.equalDomRects(newBBox, fullLayout._lastBBox)) return; + if (Lib.equalDomRects(newBBox, fullLayout._lastBBox)) return; - var m = fullLayout._invTransform = Lib.inverseTransformMatrix(Lib.getFullTransformMatrix(gd)); + 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; @@ -3679,7 +3657,8 @@ function makePlotFramework(gd) { // Plot container fullLayout._container = gd3.selectAll('.plot-container').data([0]); - fullLayout._container.enter() + fullLayout._container + .enter() .insert('div', ':first-child') .classed('plot-container', true) .classed('plotly', true) @@ -3698,13 +3677,15 @@ function makePlotFramework(gd) { // children do not have any height, because they are all positioned // absolutely, and therefore take up no space. .style({ - width: "100%", - height: "100%" + width: '100%', + height: '100%' }); // Make the svg container fullLayout._paperdiv = fullLayout._container.selectAll('.svg-container').data([0]); - fullLayout._paperdiv.enter().append('div') + fullLayout._paperdiv + .enter() + .append('div') .classed('user-select-none', true) .classed('svg-container', true) .style('position', 'relative'); @@ -3715,55 +3696,43 @@ function makePlotFramework(gd) { // 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 = fullLayout._paperdiv.selectAll('.gl-container').data([{}]); - fullLayout._glcontainer.enter().append('div') - .classed('gl-container', true); + 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._paper = fullLayout._paperdiv.insert('svg', ':first-child').classed('main-svg', true); - fullLayout._toppaper = fullLayout._paperdiv.append('svg') - .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); + fullLayout._hoverpaper = fullLayout._paperdiv.append('svg').classed('main-svg', true); - if(!fullLayout._uid) { + if (!fullLayout._uid) { var otherUids = {}; - d3.selectAll('defs').each(function() { - if(this.id) otherUids[this.id.split('-')[1]] = 1; + 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._paperdiv.selectAll('.main-svg').attr(xmlnsNamespaces.svgAttrs); - fullLayout._defs = fullLayout._paper.append('defs') - .attr('id', 'defs-' + fullLayout._uid); + fullLayout._defs = fullLayout._paper.append('defs').attr('id', 'defs-' + fullLayout._uid); - fullLayout._clips = fullLayout._defs.append('g') - .classed('clips', true); + fullLayout._clips = fullLayout._defs.append('g').classed('clips', true); - fullLayout._topdefs = fullLayout._toppaper.append('defs') - .attr('id', 'topdefs-' + fullLayout._uid); + fullLayout._topdefs = fullLayout._toppaper.append('defs').attr('id', 'topdefs-' + fullLayout._uid); - fullLayout._topclips = fullLayout._topdefs.append('g') - .classed('clips', true); + fullLayout._topclips = fullLayout._topdefs.append('g').classed('clips', true); - fullLayout._bgLayer = fullLayout._paper.append('g') - .classed('bglayer', true); + fullLayout._bgLayer = fullLayout._paper.append('g').classed('bglayer', true); - fullLayout._draggers = fullLayout._paper.append('g') - .classed('draglayer', 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 @@ -3773,12 +3742,9 @@ function makePlotFramework(gd) { // 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); + 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); @@ -3820,12 +3786,9 @@ function makePlotFramework(gd) { // 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); + 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); diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index 0650bc8ddaa..65dbc50f27d 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -22,254 +22,267 @@ var negateIf = require('../assets/negate_if'); var checkTicks = require('../assets/custom_assertions').checkTicks; var supplyAllDefaults = require('../assets/supply_defaults'); -describe('Test plot api', function() { +describe('Test plot api', function () { 'use strict'; - describe('Plotly.version', function() { - it('should be the same as in the package.json', function() { + describe('Plotly.version', function () { + it('should be the same as in the package.json', function () { expect(Plotly.version).toEqual(pkg.version); }); }); - describe('Plotly.newPlot', function() { + describe('Plotly.newPlot', function () { var gd; - beforeEach(function() { + beforeEach(function () { gd = createGraphDiv(); }); afterEach(destroyGraphDiv); - it('accepts gd, data, layout, and config as args', function(done) { - Plotly.newPlot(gd, - [{x: [1, 2, 3], y: [1, 2, 3]}], - {width: 500, height: 500}, - {editable: true} - ).then(function() { - expect(gd.layout.width).toEqual(500); - expect(gd.layout.height).toEqual(500); - expect(gd.data.length).toEqual(1); - expect(gd._context.editable).toBe(true); - }) - .then(done, done.fail); + it('accepts gd, data, layout, and config as args', function (done) { + Plotly.newPlot(gd, [{ x: [1, 2, 3], y: [1, 2, 3] }], { width: 500, height: 500 }, { editable: true }) + .then(function () { + expect(gd.layout.width).toEqual(500); + expect(gd.layout.height).toEqual(500); + expect(gd.data.length).toEqual(1); + expect(gd._context.editable).toBe(true); + }) + .then(done, done.fail); }); - it('accepts gd and an object as args', function(done) { + it('accepts gd and an object as args', function (done) { Plotly.newPlot(gd, { - data: [{x: [1, 2, 3], y: [1, 2, 3]}], - layout: {width: 500, height: 500}, - config: {editable: true}, - frames: [{y: [2, 1, 0], name: 'frame1'}] - }).then(function() { - expect(gd.layout.width).toEqual(500); - expect(gd.layout.height).toEqual(500); - expect(gd.data.length).toEqual(1); - expect(gd._transitionData._frames.length).toEqual(1); - expect(gd._context.editable).toBe(true); - }) - .then(done, done.fail); + data: [{ x: [1, 2, 3], y: [1, 2, 3] }], + layout: { width: 500, height: 500 }, + config: { editable: true }, + frames: [{ y: [2, 1, 0], name: 'frame1' }] + }) + .then(function () { + expect(gd.layout.width).toEqual(500); + expect(gd.layout.height).toEqual(500); + expect(gd.data.length).toEqual(1); + expect(gd._transitionData._frames.length).toEqual(1); + expect(gd._context.editable).toBe(true); + }) + .then(done, done.fail); }); - it('allows adding more frames to the initial set', function(done) { + it('allows adding more frames to the initial set', function (done) { Plotly.newPlot(gd, { - data: [{x: [1, 2, 3], y: [1, 2, 3]}], - layout: {width: 500, height: 500}, - config: {editable: true}, - frames: [{y: [7, 7, 7], name: 'frame1'}] - }).then(function() { - expect(gd.layout.width).toEqual(500); - expect(gd.layout.height).toEqual(500); - expect(gd.data.length).toEqual(1); - expect(gd._transitionData._frames.length).toEqual(1); - expect(gd._context.editable).toBe(true); - - return Plotly.addFrames(gd, [ - {y: [8, 8, 8], name: 'frame2'}, - {y: [9, 9, 9], name: 'frame3'} - ]); - }).then(function() { - expect(gd._transitionData._frames.length).toEqual(3); - expect(gd._transitionData._frames[0].name).toEqual('frame1'); - expect(gd._transitionData._frames[1].name).toEqual('frame2'); - expect(gd._transitionData._frames[2].name).toEqual('frame3'); - }) - .then(done, done.fail); + data: [{ x: [1, 2, 3], y: [1, 2, 3] }], + layout: { width: 500, height: 500 }, + config: { editable: true }, + frames: [{ y: [7, 7, 7], name: 'frame1' }] + }) + .then(function () { + expect(gd.layout.width).toEqual(500); + expect(gd.layout.height).toEqual(500); + expect(gd.data.length).toEqual(1); + expect(gd._transitionData._frames.length).toEqual(1); + expect(gd._context.editable).toBe(true); + + return Plotly.addFrames(gd, [ + { y: [8, 8, 8], name: 'frame2' }, + { y: [9, 9, 9], name: 'frame3' } + ]); + }) + .then(function () { + expect(gd._transitionData._frames.length).toEqual(3); + expect(gd._transitionData._frames[0].name).toEqual('frame1'); + expect(gd._transitionData._frames[1].name).toEqual('frame2'); + expect(gd._transitionData._frames[2].name).toEqual('frame3'); + }) + .then(done, done.fail); }); - it('should emit afterplot event after plotting is done', function(done) { + it('should emit afterplot event after plotting is done', function (done) { var afterPlot = false; - var promise = Plotly.newPlot(gd, [{ y: [2, 1, 2]}]); + var promise = Plotly.newPlot(gd, [{ y: [2, 1, 2] }]); - gd.on('plotly_afterplot', function() { + gd.on('plotly_afterplot', function () { afterPlot = true; }); - promise.then(function() { - expect(afterPlot).toBe(true); - }) - .then(done, done.fail); + promise + .then(function () { + expect(afterPlot).toBe(true); + }) + .then(done, done.fail); }); }); - describe('Plotly.relayout', function() { + describe('Plotly.relayout', function () { var gd; - beforeEach(function() { + beforeEach(function () { gd = createGraphDiv(); // some of these tests use the undo/redo queue // OK, this is weird... the undo/redo queue length is a global config only. // It's ignored on the plot, even though the queue itself is per-plot. // We may ditch this later, but probably not until v3 - Plotly.setPlotConfig({queueLength: 3}); + Plotly.setPlotConfig({ queueLength: 3 }); }); - afterEach(function() { + afterEach(function () { destroyGraphDiv(); - Plotly.setPlotConfig({queueLength: 0}); + Plotly.setPlotConfig({ queueLength: 0 }); }); - it('should update the plot clipPath if the plot is resized', function(done) { + it('should update the plot clipPath if the plot is resized', function (done) { Plotly.newPlot(gd, [{ x: [1, 2, 3], y: [1, 2, 3] }], { width: 500, height: 500 }) - .then(function() { - return Plotly.relayout(gd, { width: 400, height: 400 }); - }) - .then(function() { - var uid = gd._fullLayout._uid; + .then(function () { + return Plotly.relayout(gd, { width: 400, height: 400 }); + }) + .then(function () { + var uid = gd._fullLayout._uid; - var plotClip = document.getElementById('clip' + uid + 'xyplot'); - var clipRect = plotClip.children[0]; - var clipWidth = +clipRect.getAttribute('width'); - var clipHeight = +clipRect.getAttribute('height'); + var plotClip = document.getElementById('clip' + uid + 'xyplot'); + var clipRect = plotClip.children[0]; + var clipWidth = +clipRect.getAttribute('width'); + var clipHeight = +clipRect.getAttribute('height'); - expect(clipWidth).toBe(240); - expect(clipHeight).toBe(220); - }) - .then(done, done.fail); + expect(clipWidth).toBe(240); + expect(clipHeight).toBe(220); + }) + .then(done, done.fail); }); - it('sets null values to their default', function(done) { + it('sets null values to their default', function (done) { var defaultWidth; Plotly.newPlot(gd, [{ x: [1, 2, 3], y: [1, 2, 3] }]) - .then(function() { - defaultWidth = gd._fullLayout.width; - return Plotly.relayout(gd, { width: defaultWidth - 25}); - }) - .then(function() { - expect(gd._fullLayout.width).toBe(defaultWidth - 25); - return Plotly.relayout(gd, { width: null }); - }) - .then(function() { - expect(gd._fullLayout.width).toBe(defaultWidth); - }) - .then(done, done.fail); + .then(function () { + defaultWidth = gd._fullLayout.width; + return Plotly.relayout(gd, { width: defaultWidth - 25 }); + }) + .then(function () { + expect(gd._fullLayout.width).toBe(defaultWidth - 25); + return Plotly.relayout(gd, { width: null }); + }) + .then(function () { + expect(gd._fullLayout.width).toBe(defaultWidth); + }) + .then(done, done.fail); }); - it('ignores undefined values', function(done) { + it('ignores undefined values', function (done) { var defaultWidth; Plotly.newPlot(gd, [{ x: [1, 2, 3], y: [1, 2, 3] }]) - .then(function() { - defaultWidth = gd._fullLayout.width; - return Plotly.relayout(gd, { width: defaultWidth - 25}); - }) - .then(function() { - expect(gd._fullLayout.width).toBe(defaultWidth - 25); - return Plotly.relayout(gd, { width: undefined }); - }) - .then(function() { - expect(gd._fullLayout.width).toBe(defaultWidth - 25); - }) - .then(done, done.fail); + .then(function () { + defaultWidth = gd._fullLayout.width; + return Plotly.relayout(gd, { width: defaultWidth - 25 }); + }) + .then(function () { + expect(gd._fullLayout.width).toBe(defaultWidth - 25); + return Plotly.relayout(gd, { width: undefined }); + }) + .then(function () { + expect(gd._fullLayout.width).toBe(defaultWidth - 25); + }) + .then(done, done.fail); }); - it('can set items in array objects', function(done) { + it('can set items in array objects', function (done) { Plotly.newPlot(gd, [{ x: [1, 2, 3], y: [1, 2, 3] }]) - .then(function() { - return Plotly.relayout(gd, {rando: [1, 2, 3]}); - }) - .then(function() { - expect(gd.layout.rando).toEqual([1, 2, 3]); - return Plotly.relayout(gd, {'rando[1]': 45}); - }) - .then(function() { - expect(gd.layout.rando).toEqual([1, 45, 3]); - }) - .then(done, done.fail); + .then(function () { + return Plotly.relayout(gd, { rando: [1, 2, 3] }); + }) + .then(function () { + expect(gd.layout.rando).toEqual([1, 2, 3]); + return Plotly.relayout(gd, { 'rando[1]': 45 }); + }) + .then(function () { + expect(gd.layout.rando).toEqual([1, 45, 3]); + }) + .then(done, done.fail); }); - it('errors if child and parent are edited together', function(done) { - var edit1 = {rando: [{a: 1}, {b: 2}]}; - var edit2 = {'rando[1]': {c: 3}}; - var edit3 = {'rando[1].d': 4}; + it('errors if child and parent are edited together', function (done) { + var edit1 = { rando: [{ a: 1 }, { b: 2 }] }; + var edit2 = { 'rando[1]': { c: 3 } }; + var edit3 = { 'rando[1].d': 4 }; Plotly.newPlot(gd, [{ x: [1, 2, 3], y: [1, 2, 3] }]) - .then(function() { - return Plotly.relayout(gd, edit1); - }) - .then(function() { - expect(gd.layout.rando).toEqual([{a: 1}, {b: 2}]); - return Plotly.relayout(gd, edit2); - }) - .then(function() { - expect(gd.layout.rando).toEqual([{a: 1}, {c: 3}]); - return Plotly.relayout(gd, edit3); - }) - .then(function() { - expect(gd.layout.rando).toEqual([{a: 1}, {c: 3, d: 4}]); - - // OK, setup is done - test the failing combinations - [[edit1, edit2], [edit1, edit3], [edit2, edit3]].forEach(function(v) { - // combine properties in both orders - which results in the same object - // but the properties are iterated in opposite orders - expect(function() { - return Plotly.relayout(gd, Lib.extendFlat({}, v[0], v[1])); - }).toThrow(); - expect(function() { - return Plotly.relayout(gd, Lib.extendFlat({}, v[1], v[0])); - }).toThrow(); - }); - }) - .then(done, done.fail); + .then(function () { + return Plotly.relayout(gd, edit1); + }) + .then(function () { + expect(gd.layout.rando).toEqual([{ a: 1 }, { b: 2 }]); + return Plotly.relayout(gd, edit2); + }) + .then(function () { + expect(gd.layout.rando).toEqual([{ a: 1 }, { c: 3 }]); + return Plotly.relayout(gd, edit3); + }) + .then(function () { + expect(gd.layout.rando).toEqual([{ a: 1 }, { c: 3, d: 4 }]); + + // OK, setup is done - test the failing combinations + [ + [edit1, edit2], + [edit1, edit3], + [edit2, edit3] + ].forEach(function (v) { + // combine properties in both orders - which results in the same object + // but the properties are iterated in opposite orders + expect(function () { + return Plotly.relayout(gd, Lib.extendFlat({}, v[0], v[1])); + }).toThrow(); + expect(function () { + return Plotly.relayout(gd, Lib.extendFlat({}, v[1], v[0])); + }).toThrow(); + }); + }) + .then(done, done.fail); }); - it('can set empty text nodes', function(done) { - var data = [{ - x: [1, 2, 3], - y: [0, 0, 0], - text: ['', 'Text', ''], - mode: 'lines+text' - }]; + it('can set empty text nodes', function (done) { + var data = [ + { + x: [1, 2, 3], + y: [0, 0, 0], + text: ['', 'Text', ''], + mode: 'lines+text' + } + ]; var scatter = null; var oldHeight = 0; Plotly.newPlot(gd, data) - .then(function() { - scatter = document.getElementsByClassName('scatter')[0]; - oldHeight = scatter.getBoundingClientRect().height; - return Plotly.relayout(gd, 'yaxis.range', [0.5, 0.5, 0.5]); - }) - .then(function() { - var newHeight = scatter.getBoundingClientRect().height; - expect(newHeight).toEqual(oldHeight); - }) - .then(done, done.fail); + .then(function () { + scatter = document.getElementsByClassName('scatter')[0]; + oldHeight = scatter.getBoundingClientRect().height; + return Plotly.relayout(gd, 'yaxis.range', [0.5, 0.5, 0.5]); + }) + .then(function () { + var newHeight = scatter.getBoundingClientRect().height; + expect(newHeight).toEqual(oldHeight); + }) + .then(done, done.fail); }); - it('should skip empty axis objects', function(done) { - Plotly.newPlot(gd, [{ - x: [1, 2, 3], - y: [1, 2, 1] - }], { - xaxis: { title: { text: 'x title' } }, - yaxis: { title: { text: 'y title' } } - }) - .then(function() { - return Plotly.relayout(gd, { zaxis: {} }); - }) - .then(done, done.fail); + it('should skip empty axis objects', function (done) { + Plotly.newPlot( + gd, + [ + { + x: [1, 2, 3], + y: [1, 2, 1] + } + ], + { + xaxis: { title: { text: 'x title' } }, + yaxis: { title: { text: 'y title' } } + } + ) + .then(function () { + return Plotly.relayout(gd, { zaxis: {} }); + }) + .then(done, done.fail); }); - it('annotations, shapes and images linked to category axes should update properly on zoom/pan', function(done) { + it('annotations, shapes and images linked to category axes should update properly on zoom/pan', function (done) { var jsLogo = 'https://images.plot.ly/language-icons/api-home/js-logo.png'; function getPos(sel) { @@ -289,220 +302,232 @@ describe('Test plot api', function() { return getPos(d3Select('.layer-above').select('.imagelayer').select('image')); } - Plotly.newPlot(gd, [{ - x: ['a', 'b', 'c'], - y: [1, 2, 1] - }], { - xaxis: {range: [-1, 5]}, - annotations: [{ - xref: 'x', - yref: 'y', - x: 'b', - y: 2 - }], - shapes: [{ - xref: 'x', - yref: 'y', - type: 'line', - x0: 'c', - x1: 'c', - y0: -1, - y1: 4 - }], - images: [{ - xref: 'x', - yref: 'y', - source: jsLogo, - x: 'a', - y: 1, - sizex: 0.2, - sizey: 0.2 - }] - }) - .then(function() { - expect(getAnnotationPos()).toBeCloseToArray([247.5, 210.1], -0.5); - expect(getShapePos()).toBeCloseToArray([350, 369]); - expect(getImagePos()).toBeCloseToArray([170, 272.52]); + Plotly.newPlot( + gd, + [ + { + x: ['a', 'b', 'c'], + y: [1, 2, 1] + } + ], + { + xaxis: { range: [-1, 5] }, + annotations: [ + { + xref: 'x', + yref: 'y', + x: 'b', + y: 2 + } + ], + shapes: [ + { + xref: 'x', + yref: 'y', + type: 'line', + x0: 'c', + x1: 'c', + y0: -1, + y1: 4 + } + ], + images: [ + { + xref: 'x', + yref: 'y', + source: jsLogo, + x: 'a', + y: 1, + sizex: 0.2, + sizey: 0.2 + } + ] + } + ) + .then(function () { + expect(getAnnotationPos()).toBeCloseToArray([247.5, 210.1], -0.5); + expect(getShapePos()).toBeCloseToArray([350, 369]); + expect(getImagePos()).toBeCloseToArray([170, 272.52]); - return Plotly.relayout(gd, 'xaxis.range', [0, 2]); - }) - .then(function() { - expect(getAnnotationPos()).toBeCloseToArray([337.5, 210.1], -0.5); - expect(getShapePos()).toBeCloseToArray([620, 369]); - expect(getImagePos()).toBeCloseToArray([80, 272.52]); + return Plotly.relayout(gd, 'xaxis.range', [0, 2]); + }) + .then(function () { + expect(getAnnotationPos()).toBeCloseToArray([337.5, 210.1], -0.5); + expect(getShapePos()).toBeCloseToArray([620, 369]); + expect(getImagePos()).toBeCloseToArray([80, 272.52]); - return Plotly.relayout(gd, 'xaxis.range', [-1, 5]); - }) - .then(function() { - expect(getAnnotationPos()).toBeCloseToArray([247.5, 210.1], -0.5); - expect(getShapePos()).toBeCloseToArray([350, 369]); - expect(getImagePos()).toBeCloseToArray([170, 272.52]); - }) - .then(done, done.fail); + return Plotly.relayout(gd, 'xaxis.range', [-1, 5]); + }) + .then(function () { + expect(getAnnotationPos()).toBeCloseToArray([247.5, 210.1], -0.5); + expect(getShapePos()).toBeCloseToArray([350, 369]); + expect(getImagePos()).toBeCloseToArray([170, 272.52]); + }) + .then(done, done.fail); }); - it('clears autorange when you modify a range or part of a range', function(done) { + it('clears autorange when you modify a range or part of a range', function (done) { var initialXRange; var initialYRange; - Plotly.newPlot(gd, [{x: [1, 2], y: [1, 2]}]) - .then(function() { - expect(gd.layout.xaxis.autorange).toBe(true); - expect(gd.layout.yaxis.autorange).toBe(true); + Plotly.newPlot(gd, [{ x: [1, 2], y: [1, 2] }]) + .then(function () { + expect(gd.layout.xaxis.autorange).toBe(true); + expect(gd.layout.yaxis.autorange).toBe(true); - initialXRange = gd.layout.xaxis.range.slice(); - initialYRange = gd.layout.yaxis.range.slice(); + initialXRange = gd.layout.xaxis.range.slice(); + initialYRange = gd.layout.yaxis.range.slice(); - return Plotly.relayout(gd, {'xaxis.range': [0, 1], 'yaxis.range[1]': 3}); - }) - .then(function() { - expect(gd.layout.xaxis.autorange).toBe(false); - expect(gd.layout.xaxis.range).toEqual([0, 1]); - expect(gd.layout.yaxis.autorange).toBe(false); - expect(gd.layout.yaxis.range[1]).toBe(3); + return Plotly.relayout(gd, { 'xaxis.range': [0, 1], 'yaxis.range[1]': 3 }); + }) + .then(function () { + expect(gd.layout.xaxis.autorange).toBe(false); + expect(gd.layout.xaxis.range).toEqual([0, 1]); + expect(gd.layout.yaxis.autorange).toBe(false); + expect(gd.layout.yaxis.range[1]).toBe(3); - return Plotly.relayout(gd, {'xaxis.autorange': true, 'yaxis.autorange': true}); - }) - .then(function() { - expect(gd.layout.xaxis.range).toEqual(initialXRange); - expect(gd.layout.yaxis.range).toEqual(initialYRange); + return Plotly.relayout(gd, { 'xaxis.autorange': true, 'yaxis.autorange': true }); + }) + .then(function () { + expect(gd.layout.xaxis.range).toEqual(initialXRange); + expect(gd.layout.yaxis.range).toEqual(initialYRange); - // finally, test that undoing autorange puts back the previous explicit range - return Queue.undo(gd); - }) - .then(function() { - expect(gd.layout.xaxis.autorange).toBe(false); - expect(gd.layout.xaxis.range).toEqual([0, 1]); - expect(gd.layout.yaxis.autorange).toBe(false); - expect(gd.layout.yaxis.range[1]).toBe(3); - }) - .then(done, done.fail); + // finally, test that undoing autorange puts back the previous explicit range + return Queue.undo(gd); + }) + .then(function () { + expect(gd.layout.xaxis.autorange).toBe(false); + expect(gd.layout.xaxis.range).toEqual([0, 1]); + expect(gd.layout.yaxis.autorange).toBe(false); + expect(gd.layout.yaxis.range[1]).toBe(3); + }) + .then(done, done.fail); }); - it('sets aspectmode to manual when you provide any aspectratio', function(done) { - Plotly.newPlot(gd, [{x: [1, 2], y: [1, 2], z: [1, 2], type: 'scatter3d'}]) - .then(function() { - expect(gd.layout.scene.aspectmode).toBe('auto'); + it('sets aspectmode to manual when you provide any aspectratio', function (done) { + Plotly.newPlot(gd, [{ x: [1, 2], y: [1, 2], z: [1, 2], type: 'scatter3d' }]) + .then(function () { + expect(gd.layout.scene.aspectmode).toBe('auto'); - return Plotly.relayout(gd, {'scene.aspectratio.x': 2}); - }) - .then(function() { - expect(gd.layout.scene.aspectmode).toBe('manual'); + return Plotly.relayout(gd, { 'scene.aspectratio.x': 2 }); + }) + .then(function () { + expect(gd.layout.scene.aspectmode).toBe('manual'); - return Queue.undo(gd); - }) - .then(function() { - expect(gd.layout.scene.aspectmode).toBe('auto'); - }) - .then(done, done.fail); + return Queue.undo(gd); + }) + .then(function () { + expect(gd.layout.scene.aspectmode).toBe('auto'); + }) + .then(done, done.fail); }); - it('sets tickmode to linear when you edit tick0 or dtick', function(done) { - Plotly.newPlot(gd, [{x: [1, 2], y: [1, 2]}]) - .then(function() { - expect(gd.layout.xaxis.tickmode).toBeUndefined(); - expect(gd.layout.yaxis.tickmode).toBeUndefined(); + it('sets tickmode to linear when you edit tick0 or dtick', function (done) { + Plotly.newPlot(gd, [{ x: [1, 2], y: [1, 2] }]) + .then(function () { + expect(gd.layout.xaxis.tickmode).toBeUndefined(); + expect(gd.layout.yaxis.tickmode).toBeUndefined(); - return Plotly.relayout(gd, {'xaxis.tick0': 0.23, 'yaxis.dtick': 0.34}); - }) - .then(function() { - expect(gd.layout.xaxis.tickmode).toBe('linear'); - expect(gd.layout.yaxis.tickmode).toBe('linear'); + return Plotly.relayout(gd, { 'xaxis.tick0': 0.23, 'yaxis.dtick': 0.34 }); + }) + .then(function () { + expect(gd.layout.xaxis.tickmode).toBe('linear'); + expect(gd.layout.yaxis.tickmode).toBe('linear'); - return Queue.undo(gd); - }) - .then(function() { - expect(gd.layout.xaxis.tickmode).toBeUndefined(); - expect(gd.layout.yaxis.tickmode).toBeUndefined(); + return Queue.undo(gd); + }) + .then(function () { + expect(gd.layout.xaxis.tickmode).toBeUndefined(); + expect(gd.layout.yaxis.tickmode).toBeUndefined(); - expect(gd.layout.xaxis.tick0).toBeUndefined(); - expect(gd.layout.yaxis.dtick).toBeUndefined(); - }) - .then(done, done.fail); + expect(gd.layout.xaxis.tick0).toBeUndefined(); + expect(gd.layout.yaxis.dtick).toBeUndefined(); + }) + .then(done, done.fail); }); - it('updates non-auto ranges for linear/log changes', function(done) { - Plotly.newPlot(gd, [{x: [3, 5], y: [3, 5]}], { - xaxis: {range: [1, 10]}, - yaxis: {type: 'log', range: [0, 1]} - }) - .then(function() { - return Plotly.relayout(gd, {'xaxis.type': 'log', 'yaxis.type': 'linear'}); + it('updates non-auto ranges for linear/log changes', function (done) { + Plotly.newPlot(gd, [{ x: [3, 5], y: [3, 5] }], { + xaxis: { range: [1, 10] }, + yaxis: { type: 'log', range: [0, 1] } }) - .then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray([0, 1], 5); - expect(gd.layout.yaxis.range).toBeCloseToArray([1, 10], 5); + .then(function () { + return Plotly.relayout(gd, { 'xaxis.type': 'log', 'yaxis.type': 'linear' }); + }) + .then(function () { + expect(gd.layout.xaxis.range).toBeCloseToArray([0, 1], 5); + expect(gd.layout.yaxis.range).toBeCloseToArray([1, 10], 5); - return Queue.undo(gd); - }) - .then(function() { - expect(gd.layout.xaxis.range).toBeCloseToArray([1, 10], 5); - expect(gd.layout.yaxis.range).toBeCloseToArray([0, 1], 5); - }) - .then(done, done.fail); + return Queue.undo(gd); + }) + .then(function () { + expect(gd.layout.xaxis.range).toBeCloseToArray([1, 10], 5); + expect(gd.layout.yaxis.range).toBeCloseToArray([0, 1], 5); + }) + .then(done, done.fail); }); - it('respects reversed autorange when switching linear to log', function(done) { - Plotly.newPlot(gd, [{x: [1, 2], y: [1, 2]}]) - .then(function() { - // Ideally we should change this to xaxis.autorange: 'reversed' - // but that's a weird disappearing setting used just to force - // an initial reversed autorange. Proposed v3 change at: - // https://github.com/plotly/plotly.js/issues/420#issuecomment-323435082 - return Plotly.relayout(gd, 'xaxis.reverse', true); - }) - .then(function() { - var xRange = gd.layout.xaxis.range; - expect(xRange[1]).toBeLessThan(xRange[0]); - expect(xRange[0]).toBeGreaterThan(1); + it('respects reversed autorange when switching linear to log', function (done) { + Plotly.newPlot(gd, [{ x: [1, 2], y: [1, 2] }]) + .then(function () { + // Ideally we should change this to xaxis.autorange: 'reversed' + // but that's a weird disappearing setting used just to force + // an initial reversed autorange. Proposed v3 change at: + // https://github.com/plotly/plotly.js/issues/420#issuecomment-323435082 + return Plotly.relayout(gd, 'xaxis.reverse', true); + }) + .then(function () { + var xRange = gd.layout.xaxis.range; + expect(xRange[1]).toBeLessThan(xRange[0]); + expect(xRange[0]).toBeGreaterThan(1); - return Plotly.relayout(gd, 'xaxis.type', 'log'); - }) - .then(function() { - var xRange = gd.layout.xaxis.range; - expect(xRange[1]).toBeLessThan(xRange[0]); - // make sure it's a real loggy range - expect(xRange[0]).toBeLessThan(1); - }) - .then(done, done.fail); + return Plotly.relayout(gd, 'xaxis.type', 'log'); + }) + .then(function () { + var xRange = gd.layout.xaxis.range; + expect(xRange[1]).toBeLessThan(xRange[0]); + // make sure it's a real loggy range + expect(xRange[0]).toBeLessThan(1); + }) + .then(done, done.fail); }); - it('autoranges automatically when switching to/from any other axis type than linear <-> log', function(done) { - Plotly.newPlot(gd, [{x: ['1.5', '0.8'], y: [1, 2]}], {xaxis: {range: [0.6, 1.7]}}) - .then(function() { - expect(gd.layout.xaxis.autorange).toBeUndefined(); - expect(gd._fullLayout.xaxis.type).toBe('linear'); - expect(gd.layout.xaxis.range).toEqual([0.6, 1.7]); + it('autoranges automatically when switching to/from any other axis type than linear <-> log', function (done) { + Plotly.newPlot(gd, [{ x: ['1.5', '0.8'], y: [1, 2] }], { xaxis: { range: [0.6, 1.7] } }) + .then(function () { + expect(gd.layout.xaxis.autorange).toBeUndefined(); + expect(gd._fullLayout.xaxis.type).toBe('linear'); + expect(gd.layout.xaxis.range).toEqual([0.6, 1.7]); - return Plotly.relayout(gd, 'xaxis.type', 'category'); - }) - .then(function() { - expect(gd.layout.xaxis.autorange).toBe(true); - expect(gd._fullLayout.xaxis.type).toBe('category'); - expect(gd.layout.xaxis.range[0]).toBeLessThan(0); + return Plotly.relayout(gd, 'xaxis.type', 'category'); + }) + .then(function () { + expect(gd.layout.xaxis.autorange).toBe(true); + expect(gd._fullLayout.xaxis.type).toBe('category'); + expect(gd.layout.xaxis.range[0]).toBeLessThan(0); - return Queue.undo(gd); - }) - .then(function() { - expect(gd.layout.xaxis.autorange).toBeUndefined(); - expect(gd._fullLayout.xaxis.type).toBe('linear'); - expect(gd.layout.xaxis.range).toEqual([0.6, 1.7]); - }) - .then(done, done.fail); + return Queue.undo(gd); + }) + .then(function () { + expect(gd.layout.xaxis.autorange).toBeUndefined(); + expect(gd._fullLayout.xaxis.type).toBe('linear'); + expect(gd.layout.xaxis.range).toEqual([0.6, 1.7]); + }) + .then(done, done.fail); }); - it('updates autosize/width/height correctly', function(done) { + it('updates autosize/width/height correctly', function (done) { function assertSizeAndThen(width, height, auto, msg, next) { - return function() { + return function () { expect(gd._fullLayout.width).toBe(width, msg); expect(gd._fullLayout.height).toBe(height, msg); expect(gd._fullLayout.autosize).toBe(auto, msg); - if(!auto) { + if (!auto) { expect(gd.layout.width).toBe(width, msg); expect(gd.layout.height).toBe(height, msg); } - if(next) return Plotly.relayout(gd, next); + if (next) return Plotly.relayout(gd, next); }; } @@ -510,25 +535,19 @@ describe('Test plot api', function() { gd.style.width = '543px'; gd.style.height = '432px'; - Plotly.newPlot(gd, [{y: [1, 3, 2]}]) - .then(assertSizeAndThen(543, 432, true, 'initial', - {autosize: false})) - .then(assertSizeAndThen(543, 432, false, 'autosize false with no sizes', - {autosize: true})) - .then(assertSizeAndThen(543, 432, true, 'back to autosized', - {width: 600})) - .then(assertSizeAndThen(600, 432, false, 'explicit width causes explicit height', - {width: null})) - .then(assertSizeAndThen(543, 432, true, 'removed width', - {height: 500, width: 700})) - .then(assertSizeAndThen(700, 500, false, 'explicit height and width', - {autosize: true})) - .then(assertSizeAndThen(543, 432, true, 'final back to autosize')) - .then(done, done.fail); + Plotly.newPlot(gd, [{ y: [1, 3, 2] }]) + .then(assertSizeAndThen(543, 432, true, 'initial', { autosize: false })) + .then(assertSizeAndThen(543, 432, false, 'autosize false with no sizes', { autosize: true })) + .then(assertSizeAndThen(543, 432, true, 'back to autosized', { width: 600 })) + .then(assertSizeAndThen(600, 432, false, 'explicit width causes explicit height', { width: null })) + .then(assertSizeAndThen(543, 432, true, 'removed width', { height: 500, width: 700 })) + .then(assertSizeAndThen(700, 500, false, 'explicit height and width', { autosize: true })) + .then(assertSizeAndThen(543, 432, true, 'final back to autosize')) + .then(done, done.fail); }); }); - describe('Plotly.relayout subroutines switchboard', function() { + describe('Plotly.relayout subroutines switchboard', function () { var mockedMethods = [ 'layoutReplot', 'doLegend', @@ -544,8 +563,8 @@ describe('Test plot api', function() { var gd; - beforeAll(function() { - mockedMethods.forEach(function(m) { + beforeAll(function () { + mockedMethods.forEach(function (m) { spyOn(subroutines, m); }); spyOn(Axes, 'draw'); @@ -553,7 +572,7 @@ describe('Test plot api', function() { }); function mock(gd) { - mockedMethods.forEach(function(m) { + mockedMethods.forEach(function (m) { subroutines[m].calls.reset(); }); Axes.draw.calls.reset(); @@ -561,7 +580,7 @@ describe('Test plot api', function() { supplyAllDefaults(gd); Plots.supplyDefaults.calls.reset(); Plots.doCalcdata(gd); - gd.emit = function() {}; + gd.emit = function () {}; return gd; } @@ -577,51 +596,53 @@ describe('Test plot api', function() { expect(subroutines.layoutReplot.calls.count()).toBeGreaterThan(0, msg); } - it('should trigger replot (but not recalc) when switching into select or lasso dragmode for scattergl traces', function(done) { + it('should trigger replot (but not recalc) when switching into select or lasso dragmode for scattergl traces', function (done) { gd = mock({ - data: [{ - type: 'scattergl', - x: [1, 2, 3], - y: [1, 2, 3] - }], + data: [ + { + type: 'scattergl', + x: [1, 2, 3], + y: [1, 2, 3] + } + ], layout: { dragmode: 'zoom' } }); Plotly.relayout(gd, 'dragmode', 'pan') - .then(function() { - expectModeBarOnly('pan'); + .then(function () { + expectModeBarOnly('pan'); - return Plotly.relayout(mock(gd), 'dragmode', 'lasso'); - }) - .then(function() { - expectReplot('lasso 1'); + return Plotly.relayout(mock(gd), 'dragmode', 'lasso'); + }) + .then(function () { + expectReplot('lasso 1'); - return Plotly.relayout(mock(gd), 'dragmode', 'select'); - }) - .then(function() { - expectModeBarOnly('select 1'); + return Plotly.relayout(mock(gd), 'dragmode', 'select'); + }) + .then(function () { + expectModeBarOnly('select 1'); - return Plotly.relayout(mock(gd), 'dragmode', 'lasso'); - }) - .then(function() { - expectModeBarOnly('lasso 2'); + return Plotly.relayout(mock(gd), 'dragmode', 'lasso'); + }) + .then(function () { + expectModeBarOnly('lasso 2'); - return Plotly.relayout(mock(gd), 'dragmode', 'zoom'); - }) - .then(function() { - expectModeBarOnly('zoom'); + return Plotly.relayout(mock(gd), 'dragmode', 'zoom'); + }) + .then(function () { + expectModeBarOnly('zoom'); - return Plotly.relayout(mock(gd), 'dragmode', 'select'); - }) - .then(function() { - expectReplot('select 2'); - }) - .then(done, done.fail); + return Plotly.relayout(mock(gd), 'dragmode', 'select'); + }) + .then(function () { + expectReplot('select 2'); + }) + .then(done, done.fail); }); - it('should trigger replot (but not recalc) when changing attributes that affect axis length/range', function() { + it('should trigger replot (but not recalc) when changing attributes that affect axis length/range', function () { // but axis.autorange itself is NOT here, because setting it from false to true requires an // autorange so that we calculate _min and _max, which we ignore if autorange is off. var axLayoutEdits = { @@ -646,35 +667,37 @@ describe('Test plot api', function() { 'grid.pattern': 'independent', 'grid.yaxes': ['y2', 'y'], 'grid.xaxes[0]': 'x2', - 'grid.domain': {x: [0, 0.4], y: [0.6, 1]}, + 'grid.domain': { x: [0, 0.4], y: [0.6, 1] }, 'grid.domain.x': [0.01, 0.99], 'grid.domain.y[0]': 0.33, - 'grid.subplots': [['', 'xy'], ['x2y2', '']], + 'grid.subplots': [ + ['', 'xy'], + ['x2y2', ''] + ], 'grid.subplots[1][1]': 'xy' }; var attr; var checkAttr = expectReplot(attr); - for(attr in axLayoutEdits) { + for (attr in axLayoutEdits) { gd = mock({ - data: [{y: [1, 2]}, {y: [4, 3], xaxis: 'x2', yaxis: 'y2'}], + data: [{ y: [1, 2] }, { y: [4, 3], xaxis: 'x2', yaxis: 'y2' }], layout: { - xaxis2: {domain: [0.6, 0.9]}, - yaxis2: {domain: [0.6, 0.9]} + xaxis2: { domain: [0.6, 0.9] }, + yaxis2: { domain: [0.6, 0.9] } } }); - Plotly.relayout(gd, attr, axLayoutEdits[attr]) - .then(checkAttr); + Plotly.relayout(gd, attr, axLayoutEdits[attr]).then(checkAttr); } }); - it('should trigger minimal sequence for cartesian axis range updates', function() { + it('should trigger minimal sequence for cartesian axis range updates', function () { var seq = ['doAutoRangeAndConstraints', 'drawData', 'finalDraw']; function _assert(msg) { expect(gd.calcdata).toBeDefined(); - mockedMethods.forEach(function(m) { + mockedMethods.forEach(function (m) { expect(subroutines[m].calls.count()).toBe( seq.indexOf(m) === -1 ? 0 : 1, '# of ' + m + ' calls - ' + msg @@ -682,7 +705,7 @@ describe('Test plot api', function() { }); expect(Axes.draw).toHaveBeenCalledTimes(1); expect(Axes.draw.calls.allArgs()[0][1]).toEqual(['x']); - expect(Axes.draw.calls.allArgs()[0][2]).toEqual({skipTitle: true}, 'skip-axis-title argument'); + expect(Axes.draw.calls.allArgs()[0][2]).toEqual({ skipTitle: true }, 'skip-axis-title argument'); expect(Plots.supplyDefaults).not.toHaveBeenCalled(); } @@ -690,50 +713,51 @@ describe('Test plot api', function() { ['relayout', ['xaxis.range[0]', 0]], ['relayout', ['xaxis.range[1]', 3]], ['relayout', ['xaxis.range', [-1, 5]]], - ['update', [{}, {'xaxis.range': [-1, 10]}]], + ['update', [{}, { 'xaxis.range': [-1, 10] }]], ['relayout', ['xaxis.autorange', true]], - ['update', [{}, {'xaxis.autorange': true}]] + ['update', [{}, { 'xaxis.autorange': true }]] ]; - specs.forEach(function(s) { + specs.forEach(function (s) { gd = mock({ - data: [{y: [1, 2, 1]}], + data: [{ y: [1, 2, 1] }], layout: { - xaxis: {range: [1, 2]} + xaxis: { range: [1, 2] } } }); Plotly[s[0]](gd, s[1][0], s[1][1]); - _assert([ - 'Plotly.', s[0], - '(gd, ', JSON.stringify(s[1][0]), ', ', JSON.stringify(s[1][1]), ')' - ].join('')); + _assert( + ['Plotly.', s[0], '(gd, ', JSON.stringify(s[1][0]), ', ', JSON.stringify(s[1][1]), ')'].join('') + ); }); }); - it('should trigger calc on axis range updates when constraints are present', function(done) { + it('should trigger calc on axis range updates when constraints are present', function (done) { gd = mock({ - data: [{ - y: [1, 2, 1] - }], + data: [ + { + y: [1, 2, 1] + } + ], layout: { - xaxis: {constrain: 'domain'}, - yaxis: {scaleanchor: 'x'} + xaxis: { constrain: 'domain' }, + yaxis: { scaleanchor: 'x' } } }); Plotly.relayout(gd, 'xaxis.range[0]', 0) - .then(function() { - expect(gd.calcdata).toBeUndefined(); - }) - .then(done, done.fail); + .then(function () { + expect(gd.calcdata).toBeUndefined(); + }) + .then(done, done.fail); }); }); - describe('Plotly.restyle subroutines switchboard', function() { - beforeEach(function() { + describe('Plotly.restyle subroutines switchboard', function () { + beforeEach(function () { spyOn(plotApi, '_doPlot'); spyOn(Plots, 'previousPromises'); spyOn(Scatter, 'arraysToCalcdata'); @@ -744,128 +768,128 @@ describe('Test plot api', function() { function mockDefaultsAndCalc(gd) { supplyAllDefaults(gd); - gd.calcdata = gd._fullData.map(function(trace) { - return [{x: 1, y: 1, trace: trace}]; + gd.calcdata = gd._fullData.map(function (trace) { + return [{ x: 1, y: 1, trace: trace }]; }); - gd.emit = function() {}; + gd.emit = function () {}; } - it('calls Scatter.arraysToCalcdata and Plots.style on scatter styling', function(done) { + it('calls Scatter.arraysToCalcdata and Plots.style on scatter styling', function (done) { var gd = { - data: [{x: [1, 2, 3], y: [1, 2, 3]}], + data: [{ x: [1, 2, 3], y: [1, 2, 3] }], layout: {} }; mockDefaultsAndCalc(gd); - Plotly.restyle(gd, {'marker.color': 'red'}) - .then(function() { - expect(Scatter.arraysToCalcdata).toHaveBeenCalled(); - expect(Bar.arraysToCalcdata).not.toHaveBeenCalled(); - expect(Plots.style).toHaveBeenCalled(); - expect(plotApi._doPlot).not.toHaveBeenCalled(); - // "docalc" deletes gd.calcdata - make sure this didn't happen - expect(gd.calcdata).toBeDefined(); - }) - .then(done, done.fail); + Plotly.restyle(gd, { 'marker.color': 'red' }) + .then(function () { + expect(Scatter.arraysToCalcdata).toHaveBeenCalled(); + expect(Bar.arraysToCalcdata).not.toHaveBeenCalled(); + expect(Plots.style).toHaveBeenCalled(); + expect(plotApi._doPlot).not.toHaveBeenCalled(); + // "docalc" deletes gd.calcdata - make sure this didn't happen + expect(gd.calcdata).toBeDefined(); + }) + .then(done, done.fail); }); - it('calls Bar.arraysToCalcdata and Plots.style on bar styling', function(done) { + it('calls Bar.arraysToCalcdata and Plots.style on bar styling', function (done) { var gd = { - data: [{x: [1, 2, 3], y: [1, 2, 3], type: 'bar'}], + data: [{ x: [1, 2, 3], y: [1, 2, 3], type: 'bar' }], layout: {} }; mockDefaultsAndCalc(gd); - Plotly.restyle(gd, {'marker.color': 'red'}) - .then(function() { - expect(Scatter.arraysToCalcdata).not.toHaveBeenCalled(); - expect(Bar.arraysToCalcdata).toHaveBeenCalled(); - expect(Plots.style).toHaveBeenCalled(); - expect(plotApi._doPlot).not.toHaveBeenCalled(); - expect(gd.calcdata).toBeDefined(); - }) - .then(done, done.fail); + Plotly.restyle(gd, { 'marker.color': 'red' }) + .then(function () { + expect(Scatter.arraysToCalcdata).not.toHaveBeenCalled(); + expect(Bar.arraysToCalcdata).toHaveBeenCalled(); + expect(Plots.style).toHaveBeenCalled(); + expect(plotApi._doPlot).not.toHaveBeenCalled(); + expect(gd.calcdata).toBeDefined(); + }) + .then(done, done.fail); }); - it('should do full replot when arrayOk attributes are updated', function(done) { + it('should do full replot when arrayOk attributes are updated', function (done) { var gd = { - data: [{x: [1, 2, 3], y: [1, 2, 3]}], + data: [{ x: [1, 2, 3], y: [1, 2, 3] }], layout: {} }; mockDefaultsAndCalc(gd); Plotly.restyle(gd, 'marker.color', [['red', 'green', 'blue']]) - .then(function() { - expect(gd.calcdata).toBeUndefined(); - expect(plotApi._doPlot).toHaveBeenCalled(); + .then(function () { + expect(gd.calcdata).toBeUndefined(); + expect(plotApi._doPlot).toHaveBeenCalled(); - mockDefaultsAndCalc(gd); - plotApi._doPlot.calls.reset(); - return Plotly.restyle(gd, 'marker.color', 'yellow'); - }) - .then(function() { - expect(gd.calcdata).toBeUndefined(); - expect(plotApi._doPlot).toHaveBeenCalled(); + mockDefaultsAndCalc(gd); + plotApi._doPlot.calls.reset(); + return Plotly.restyle(gd, 'marker.color', 'yellow'); + }) + .then(function () { + expect(gd.calcdata).toBeUndefined(); + expect(plotApi._doPlot).toHaveBeenCalled(); - mockDefaultsAndCalc(gd); - plotApi._doPlot.calls.reset(); - return Plotly.restyle(gd, 'marker.color', 'blue'); - }) - .then(function() { - expect(gd.calcdata).toBeDefined(); - expect(plotApi._doPlot).not.toHaveBeenCalled(); + mockDefaultsAndCalc(gd); + plotApi._doPlot.calls.reset(); + return Plotly.restyle(gd, 'marker.color', 'blue'); + }) + .then(function () { + expect(gd.calcdata).toBeDefined(); + expect(plotApi._doPlot).not.toHaveBeenCalled(); - mockDefaultsAndCalc(gd); - plotApi._doPlot.calls.reset(); - return Plotly.restyle(gd, 'marker.color', [['red', 'blue', 'green']]); - }) - .then(function() { - expect(gd.calcdata).toBeUndefined(); - expect(plotApi._doPlot).toHaveBeenCalled(); - }) - .then(done, done.fail); + mockDefaultsAndCalc(gd); + plotApi._doPlot.calls.reset(); + return Plotly.restyle(gd, 'marker.color', [['red', 'blue', 'green']]); + }) + .then(function () { + expect(gd.calcdata).toBeUndefined(); + expect(plotApi._doPlot).toHaveBeenCalled(); + }) + .then(done, done.fail); }); - it('should do full replot when arrayOk base attributes are updated', function(done) { + it('should do full replot when arrayOk base attributes are updated', function (done) { var gd = { - data: [{x: [1, 2, 3], y: [1, 2, 3]}], + data: [{ x: [1, 2, 3], y: [1, 2, 3] }], layout: {} }; mockDefaultsAndCalc(gd); Plotly.restyle(gd, 'hoverlabel.bgcolor', [['red', 'green', 'blue']]) - .then(function() { - expect(gd.calcdata).toBeUndefined(); - expect(plotApi._doPlot).toHaveBeenCalled(); + .then(function () { + expect(gd.calcdata).toBeUndefined(); + expect(plotApi._doPlot).toHaveBeenCalled(); - mockDefaultsAndCalc(gd); - plotApi._doPlot.calls.reset(); - return Plotly.restyle(gd, 'hoverlabel.bgcolor', 'yellow'); - }) - .then(function() { - expect(gd.calcdata).toBeUndefined(); - expect(plotApi._doPlot).toHaveBeenCalled(); + mockDefaultsAndCalc(gd); + plotApi._doPlot.calls.reset(); + return Plotly.restyle(gd, 'hoverlabel.bgcolor', 'yellow'); + }) + .then(function () { + expect(gd.calcdata).toBeUndefined(); + expect(plotApi._doPlot).toHaveBeenCalled(); - mockDefaultsAndCalc(gd); - plotApi._doPlot.calls.reset(); - return Plotly.restyle(gd, 'hoverlabel.bgcolor', 'blue'); - }) - .then(function() { - expect(gd.calcdata).toBeDefined(); - expect(plotApi._doPlot).not.toHaveBeenCalled(); + mockDefaultsAndCalc(gd); + plotApi._doPlot.calls.reset(); + return Plotly.restyle(gd, 'hoverlabel.bgcolor', 'blue'); + }) + .then(function () { + expect(gd.calcdata).toBeDefined(); + expect(plotApi._doPlot).not.toHaveBeenCalled(); - mockDefaultsAndCalc(gd); - plotApi._doPlot.calls.reset(); - return Plotly.restyle(gd, 'hoverlabel.bgcolor', [['red', 'blue', 'green']]); - }) - .then(function() { - expect(gd.calcdata).toBeUndefined(); - expect(plotApi._doPlot).toHaveBeenCalled(); - }) - .then(done, done.fail); + mockDefaultsAndCalc(gd); + plotApi._doPlot.calls.reset(); + return Plotly.restyle(gd, 'hoverlabel.bgcolor', [['red', 'blue', 'green']]); + }) + .then(function () { + expect(gd.calcdata).toBeUndefined(); + expect(plotApi._doPlot).toHaveBeenCalled(); + }) + .then(done, done.fail); }); - it('should do full replot when attribute container are updated', function(done) { + it('should do full replot when attribute container are updated', function (done) { var gd = { - data: [{x: [1, 2, 3], y: [1, 2, 3]}], + data: [{ x: [1, 2, 3], y: [1, 2, 3] }], layout: { xaxis: { range: [0, 4] }, yaxis: { range: [0, 4] } @@ -878,144 +902,173 @@ describe('Test plot api', function() { color: ['red', 'blue', 'green'] } }) - .then(function() { - expect(gd.calcdata).toBeUndefined(); - expect(plotApi._doPlot).toHaveBeenCalled(); - }) - .then(done, done.fail); + .then(function () { + expect(gd.calcdata).toBeUndefined(); + expect(plotApi._doPlot).toHaveBeenCalled(); + }) + .then(done, done.fail); }); - it('calls plot on xgap and ygap styling', function(done) { + it('calls plot on xgap and ygap styling', function (done) { var gd = { - data: [{z: [[1, 2, 3], [4, 5, 6], [7, 8, 9]], showscale: false, type: 'heatmap'}], + data: [ + { + z: [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9] + ], + showscale: false, + type: 'heatmap' + } + ], layout: {} }; mockDefaultsAndCalc(gd); - Plotly.restyle(gd, {xgap: 2}) - .then(function() { - expect(plotApi._doPlot).toHaveBeenCalled(); + Plotly.restyle(gd, { xgap: 2 }) + .then(function () { + expect(plotApi._doPlot).toHaveBeenCalled(); - return Plotly.restyle(gd, {ygap: 2}); - }) - .then(function() { - expect(plotApi._doPlot.calls.count()).toEqual(2); - }) - .then(done, done.fail); + return Plotly.restyle(gd, { ygap: 2 }); + }) + .then(function () { + expect(plotApi._doPlot.calls.count()).toEqual(2); + }) + .then(done, done.fail); }); [ { - data: [{ - type: 'contour', - z: [[1, 2, 3], [1, 2, 1]] - }] + data: [ + { + type: 'contour', + z: [ + [1, 2, 3], + [1, 2, 1] + ] + } + ] }, { - data: [{ - type: 'histogram2dcontour', - x: [1, 1, 2, 2, 2, 3], - y: [0, 0, 0, 0, 1, 3] - }] + data: [ + { + type: 'histogram2dcontour', + x: [1, 1, 2, 2, 2, 3], + y: [0, 0, 0, 0, 1, 3] + } + ] } - ].forEach(function(gd) { - it('should clear calcdata when restyling \'zmin\' and \'zmax\' on ' + gd.data.type + ' traces', function(done) { - mockDefaultsAndCalc(gd); - plotApi._doPlot.calls.reset(); - Plotly.restyle(gd, 'zmin', 0) - .then(function() { - expect(gd.calcdata).toBeUndefined(); - expect(plotApi._doPlot).toHaveBeenCalled(); - + ].forEach(function (gd) { + it( + "should clear calcdata when restyling 'zmin' and 'zmax' on " + gd.data.type + ' traces', + function (done) { mockDefaultsAndCalc(gd); plotApi._doPlot.calls.reset(); - return Plotly.restyle(gd, 'zmax', 10); - }) - .then(function() { - expect(gd.calcdata).toBeUndefined(); - expect(plotApi._doPlot).toHaveBeenCalled(); - }) - .then(done, done.fail); - }); + Plotly.restyle(gd, 'zmin', 0) + .then(function () { + expect(gd.calcdata).toBeUndefined(); + expect(plotApi._doPlot).toHaveBeenCalled(); + + mockDefaultsAndCalc(gd); + plotApi._doPlot.calls.reset(); + return Plotly.restyle(gd, 'zmax', 10); + }) + .then(function () { + expect(gd.calcdata).toBeUndefined(); + expect(plotApi._doPlot).toHaveBeenCalled(); + }) + .then(done, done.fail); + } + ); }); [ { - data: [{ - type: 'heatmap', - z: [[1, 2, 3], [1, 2, 1]] - }] + data: [ + { + type: 'heatmap', + z: [ + [1, 2, 3], + [1, 2, 1] + ] + } + ] }, { - data: [{ - type: 'histogram2d', - x: [1, 1, 2, 2, 2, 3], - y: [0, 0, 0, 0, 1, 3] - }] + data: [ + { + type: 'histogram2d', + x: [1, 1, 2, 2, 2, 3], + y: [0, 0, 0, 0, 1, 3] + } + ] } - ].forEach(function(gd) { - it('should not clear calcdata when restyling \'zmin\' and \'zmax\' on ' + gd.data.type + ' traces', function(done) { - mockDefaultsAndCalc(gd); - plotApi._doPlot.calls.reset(); - Plotly.restyle(gd, 'zmin', 0) - .then(function() { - expect(gd.calcdata).toBeDefined(); - expect(plotApi._doPlot).toHaveBeenCalled(); - - mockDefaultsAndCalc(gd); - plotApi._doPlot.calls.reset(); - return Plotly.restyle(gd, 'zmax', 10); - }) - .then(function() { - expect(gd.calcdata).toBeDefined(); - expect(plotApi._doPlot).toHaveBeenCalled(); - + ].forEach(function (gd) { + it( + "should not clear calcdata when restyling 'zmin' and 'zmax' on " + gd.data.type + ' traces', + function (done) { mockDefaultsAndCalc(gd); plotApi._doPlot.calls.reset(); - return Plotly.restyle(gd, 'zmin', 0); - }) - .then(done, done.fail); - }); + Plotly.restyle(gd, 'zmin', 0) + .then(function () { + expect(gd.calcdata).toBeDefined(); + expect(plotApi._doPlot).toHaveBeenCalled(); + + mockDefaultsAndCalc(gd); + plotApi._doPlot.calls.reset(); + return Plotly.restyle(gd, 'zmax', 10); + }) + .then(function () { + expect(gd.calcdata).toBeDefined(); + expect(plotApi._doPlot).toHaveBeenCalled(); + + mockDefaultsAndCalc(gd); + plotApi._doPlot.calls.reset(); + return Plotly.restyle(gd, 'zmin', 0); + }) + .then(done, done.fail); + } + ); }); - it('ignores undefined values', function(done) { + it('ignores undefined values', function (done) { var gd = { - data: [{x: [1, 2, 3], y: [1, 2, 3], type: 'scatter'}], + data: [{ x: [1, 2, 3], y: [1, 2, 3], type: 'scatter' }], layout: {} }; mockDefaultsAndCalc(gd); // Check to see that the color is updated: - Plotly.restyle(gd, {'marker.color': 'blue'}) - .then(function() { - expect(gd._fullData[0].marker.color).toBe('blue'); + Plotly.restyle(gd, { 'marker.color': 'blue' }) + .then(function () { + expect(gd._fullData[0].marker.color).toBe('blue'); - // Check to see that the color is unaffected: - return Plotly.restyle(gd, {'marker.color': undefined}); - }) - .then(function() { - expect(gd._fullData[0].marker.color).toBe('blue'); - }) - .then(done, done.fail); + // Check to see that the color is unaffected: + return Plotly.restyle(gd, { 'marker.color': undefined }); + }) + .then(function () { + expect(gd._fullData[0].marker.color).toBe('blue'); + }) + .then(done, done.fail); }); - it('ignores invalid trace indices', function(done) { + it('ignores invalid trace indices', function (done) { var gd = { - data: [{x: [1, 2, 3], y: [1, 2, 3], type: 'scatter'}], + data: [{ x: [1, 2, 3], y: [1, 2, 3], type: 'scatter' }], layout: {} }; mockDefaultsAndCalc(gd); // Call restyle on an invalid trace indice - Plotly.restyle(gd, {type: 'scatter', 'marker.color': 'red'}, [1]) - .then(done, done.fail); + Plotly.restyle(gd, { type: 'scatter', 'marker.color': 'red' }, [1]).then(done, done.fail); }); - it('restores null values to defaults', function(done) { + it('restores null values to defaults', function (done) { var gd = { - data: [{x: [1, 2, 3], y: [1, 2, 3], type: 'scatter'}], + data: [{ x: [1, 2, 3], y: [1, 2, 3], type: 'scatter' }], layout: {} }; @@ -1023,24 +1076,24 @@ describe('Test plot api', function() { var colorDflt = gd._fullData[0].marker.color; // Check to see that the color is updated: - Plotly.restyle(gd, {'marker.color': 'blue'}) - .then(function() { - expect(gd._fullData[0].marker.color).toBe('blue'); + Plotly.restyle(gd, { 'marker.color': 'blue' }) + .then(function () { + expect(gd._fullData[0].marker.color).toBe('blue'); - // Check to see that the color is restored to the original default: - return Plotly.restyle(gd, {'marker.color': null}); - }) - .then(function() { - expect(gd._fullData[0].marker.color).toBe(colorDflt); - }) - .then(done, done.fail); + // Check to see that the color is restored to the original default: + return Plotly.restyle(gd, { 'marker.color': null }); + }) + .then(function () { + expect(gd._fullData[0].marker.color).toBe(colorDflt); + }) + .then(done, done.fail); }); - it('can target specific traces by leaving properties undefined', function(done) { + it('can target specific traces by leaving properties undefined', function (done) { var gd = { data: [ - {x: [1, 2, 3], y: [1, 2, 3], type: 'scatter'}, - {x: [1, 2, 3], y: [3, 4, 5], type: 'scatter'} + { x: [1, 2, 3], y: [1, 2, 3], type: 'scatter' }, + { x: [1, 2, 3], y: [3, 4, 5], type: 'scatter' } ], layout: {} }; @@ -1049,94 +1102,108 @@ describe('Test plot api', function() { var colorDflt = [gd._fullData[0].marker.color, gd._fullData[1].marker.color]; // Check only second trace's color has been changed: - Plotly.restyle(gd, {'marker.color': [undefined, 'green']}) - .then(function() { - expect(gd._fullData[0].marker.color).toBe(colorDflt[0]); - expect(gd._fullData[1].marker.color).toBe('green'); + Plotly.restyle(gd, { 'marker.color': [undefined, 'green'] }) + .then(function () { + expect(gd._fullData[0].marker.color).toBe(colorDflt[0]); + expect(gd._fullData[1].marker.color).toBe('green'); - // Check both colors restored to the original default: - return Plotly.restyle(gd, {'marker.color': [null, null]}); - }) - .then(function() { - expect(gd._fullData[0].marker.color).toBe(colorDflt[0]); - expect(gd._fullData[1].marker.color).toBe(colorDflt[1]); - }) - .then(done, done.fail); + // Check both colors restored to the original default: + return Plotly.restyle(gd, { 'marker.color': [null, null] }); + }) + .then(function () { + expect(gd._fullData[0].marker.color).toBe(colorDflt[0]); + expect(gd._fullData[1].marker.color).toBe(colorDflt[1]); + }) + .then(done, done.fail); }); }); - describe('Plotly.restyle unmocked', function() { + describe('Plotly.restyle unmocked', function () { var gd; - beforeEach(function() { + beforeEach(function () { gd = createGraphDiv(); // some of these tests use the undo/redo queue // OK, this is weird... the undo/redo queue length is a global config only. // It's ignored on the plot, even though the queue itself is per-plot. // We may ditch this later, but probably not until v3 - Plotly.setPlotConfig({queueLength: 3}); + Plotly.setPlotConfig({ queueLength: 3 }); }); - afterEach(function() { + afterEach(function () { destroyGraphDiv(); - Plotly.setPlotConfig({queueLength: 0}); - }); - - it('should redo auto z/contour when editing z array', function(done) { - Plotly.newPlot(gd, [{type: 'contour', z: [[1, 2], [3, 4]]}]).then(function() { - expect(gd._fullData[0].zauto).toBe(true); - expect(gd._fullData[0].zmin).toBe(1); - expect(gd._fullData[0].zmax).toBe(4); - expect(gd.data[0].autocontour).toBe(true); - expect(gd.data[0].contours).toEqual({start: 1.5, end: 3.5, size: 0.5}); - - return Plotly.restyle(gd, {'z[0][0]': 10}); - }).then(function() { - expect(gd._fullData[0].zauto).toBe(true); - expect(gd._fullData[0].zmin).toBe(2); - expect(gd._fullData[0].zmax).toBe(10); - expect(gd.data[0].contours).toEqual({start: 3, end: 9, size: 1}); - }) - .then(done, done.fail); + Plotly.setPlotConfig({ queueLength: 0 }); + }); + + it('should redo auto z/contour when editing z array', function (done) { + Plotly.newPlot(gd, [ + { + type: 'contour', + z: [ + [1, 2], + [3, 4] + ] + } + ]) + .then(function () { + expect(gd._fullData[0].zauto).toBe(true); + expect(gd._fullData[0].zmin).toBe(1); + expect(gd._fullData[0].zmax).toBe(4); + expect(gd.data[0].autocontour).toBe(true); + expect(gd.data[0].contours).toEqual({ start: 1.5, end: 3.5, size: 0.5 }); + + return Plotly.restyle(gd, { 'z[0][0]': 10 }); + }) + .then(function () { + expect(gd._fullData[0].zauto).toBe(true); + expect(gd._fullData[0].zmin).toBe(2); + expect(gd._fullData[0].zmax).toBe(10); + expect(gd.data[0].contours).toEqual({ start: 3, end: 9, size: 1 }); + }) + .then(done, done.fail); }); - it('errors if child and parent are edited together', function(done) { - var edit1 = {rando: [[{a: 1}, {b: 2}]]}; - var edit2 = {'rando[1]': {c: 3}}; - var edit3 = {'rando[1].d': 4}; + it('errors if child and parent are edited together', function (done) { + var edit1 = { rando: [[{ a: 1 }, { b: 2 }]] }; + var edit2 = { 'rando[1]': { c: 3 } }; + var edit3 = { 'rando[1].d': 4 }; - Plotly.newPlot(gd, [{x: [1, 2, 3], y: [1, 2, 3], type: 'scatter'}]) - .then(function() { - return Plotly.restyle(gd, edit1); - }) - .then(function() { - expect(gd.data[0].rando).toEqual([{a: 1}, {b: 2}]); - return Plotly.restyle(gd, edit2); - }) - .then(function() { - expect(gd.data[0].rando).toEqual([{a: 1}, {c: 3}]); - return Plotly.restyle(gd, edit3); - }) - .then(function() { - expect(gd.data[0].rando).toEqual([{a: 1}, {c: 3, d: 4}]); - - // OK, setup is done - test the failing combinations - [[edit1, edit2], [edit1, edit3], [edit2, edit3]].forEach(function(v) { - // combine properties in both orders - which results in the same object - // but the properties are iterated in opposite orders - expect(function() { - return Plotly.restyle(gd, Lib.extendFlat({}, v[0], v[1])); - }).toThrow(); - expect(function() { - return Plotly.restyle(gd, Lib.extendFlat({}, v[1], v[0])); - }).toThrow(); - }); - }) - .then(done, done.fail); + Plotly.newPlot(gd, [{ x: [1, 2, 3], y: [1, 2, 3], type: 'scatter' }]) + .then(function () { + return Plotly.restyle(gd, edit1); + }) + .then(function () { + expect(gd.data[0].rando).toEqual([{ a: 1 }, { b: 2 }]); + return Plotly.restyle(gd, edit2); + }) + .then(function () { + expect(gd.data[0].rando).toEqual([{ a: 1 }, { c: 3 }]); + return Plotly.restyle(gd, edit3); + }) + .then(function () { + expect(gd.data[0].rando).toEqual([{ a: 1 }, { c: 3, d: 4 }]); + + // OK, setup is done - test the failing combinations + [ + [edit1, edit2], + [edit1, edit3], + [edit2, edit3] + ].forEach(function (v) { + // combine properties in both orders - which results in the same object + // but the properties are iterated in opposite orders + expect(function () { + return Plotly.restyle(gd, Lib.extendFlat({}, v[0], v[1])); + }).toThrow(); + expect(function () { + return Plotly.restyle(gd, Lib.extendFlat({}, v[1], v[0])); + }).toThrow(); + }); + }) + .then(done, done.fail); }); - it('turns off zauto when you edit zmin or zmax', function(done) { + it('turns off zauto when you edit zmin or zmax', function (done) { var zmin0 = 2; var zmax1 = 10; @@ -1148,31 +1215,44 @@ describe('Test plot api', function() { } Plotly.newPlot(gd, [ - {z: [[1, 2], [3, 4]], type: 'heatmap'}, - {x: [2, 3], z: [[5, 6], [7, 8]], type: 'contour'} + { + z: [ + [1, 2], + [3, 4] + ], + type: 'heatmap' + }, + { + x: [2, 3], + z: [ + [5, 6], + [7, 8] + ], + type: 'contour' + } ]) - .then(function() { - check(true, 'initial'); - return Plotly.restyle(gd, 'zmin', zmin0, [0]); - }) - .then(function() { - return Plotly.restyle(gd, 'zmax', zmax1, [1]); - }) - .then(function() { - check(false, 'set min/max'); - return Plotly.restyle(gd, 'zauto', true); - }) - .then(function() { - check(true, 'reset'); - return Queue.undo(gd); - }) - .then(function() { - check(false, 'undo'); - }) - .then(done, done.fail); + .then(function () { + check(true, 'initial'); + return Plotly.restyle(gd, 'zmin', zmin0, [0]); + }) + .then(function () { + return Plotly.restyle(gd, 'zmax', zmax1, [1]); + }) + .then(function () { + check(false, 'set min/max'); + return Plotly.restyle(gd, 'zauto', true); + }) + .then(function () { + check(true, 'reset'); + return Queue.undo(gd); + }) + .then(function () { + check(false, 'undo'); + }) + .then(done, done.fail); }); - it('turns off cauto (autocolorscale) when you edit cmin or cmax (colorscale)', function(done) { + it('turns off cauto (autocolorscale) when you edit cmin or cmax (colorscale)', function (done) { var scales = require('../../../src/components/colorscale/scales').scales; var autocscale = scales.Reds; @@ -1194,107 +1274,135 @@ describe('Test plot api', function() { } Plotly.newPlot(gd, [ - {y: [1, 2], mode: 'markers', marker: {color: [1, 10]}}, - {y: [2, 1], mode: 'markers', marker: {line: {width: 2, color: [3, 4]}}} + { y: [1, 2], mode: 'markers', marker: { color: [1, 10] } }, + { y: [2, 1], mode: 'markers', marker: { line: { width: 2, color: [3, 4] } } } ]) - .then(function() { - check(true, true, 'initial'); - return Plotly.restyle(gd, {'marker.cmin': mcmin0, 'marker.colorscale': mcscl0}, null, [0]); - }) - .then(function() { - return Plotly.restyle(gd, {'marker.line.cmax': mlcmax1, 'marker.line.colorscale': mlcscl1}, null, [1]); - }) - .then(function() { - check(false, false, 'set min/max/scale'); - return Plotly.restyle(gd, {'marker.cauto': true, 'marker.autocolorscale': true}, null, [0]); - }) - .then(function() { - return Plotly.restyle(gd, {'marker.line.cauto': true, 'marker.line.autocolorscale': true}, null, [1]); - }) - .then(function() { - check(true, true, 'reset'); - return Queue.undo(gd); - }) - .then(function() { - return Queue.undo(gd); - }) - .then(function() { - check(false, false, 'undo'); - }) - .then(done, done.fail); + .then(function () { + check(true, true, 'initial'); + return Plotly.restyle(gd, { 'marker.cmin': mcmin0, 'marker.colorscale': mcscl0 }, null, [0]); + }) + .then(function () { + return Plotly.restyle( + gd, + { 'marker.line.cmax': mlcmax1, 'marker.line.colorscale': mlcscl1 }, + null, + [1] + ); + }) + .then(function () { + check(false, false, 'set min/max/scale'); + return Plotly.restyle(gd, { 'marker.cauto': true, 'marker.autocolorscale': true }, null, [0]); + }) + .then(function () { + return Plotly.restyle( + gd, + { 'marker.line.cauto': true, 'marker.line.autocolorscale': true }, + null, + [1] + ); + }) + .then(function () { + check(true, true, 'reset'); + return Queue.undo(gd); + }) + .then(function () { + return Queue.undo(gd); + }) + .then(function () { + check(false, false, 'undo'); + }) + .then(done, done.fail); }); - it('turns on cauto when cmid is edited', function(done) { + it('turns on cauto when cmid is edited', function (done) { function _assert(msg, exp) { - return function() { + return function () { var mk = gd._fullData[0].marker; - for(var k in exp) { + for (var k in exp) { expect(mk[k]).toBe(exp[k], [msg, k].join(' - ')); } }; } function _restyle(arg) { - return function() { return Plotly.restyle(gd, arg); }; + return function () { + return Plotly.restyle(gd, arg); + }; } - Plotly.newPlot(gd, [{ - mode: 'markers', - y: [1, 2, 1], - marker: { color: [1, -1, 4] } - }]) - .then(_assert('base', { - cauto: true, - cmid: undefined, - cmin: -1, - cmax: 4 - })) - .then(_restyle({'marker.cmid': 0})) - .then(_assert('set cmid=0', { - cauto: true, - cmid: 0, - cmin: -4, - cmax: 4 - })) - .then(_restyle({'marker.cmid': -2})) - .then(_assert('set cmid=-2', { - cauto: true, - cmid: -2, - cmin: -8, - cmax: 4 - })) - .then(_restyle({'marker.cmid': 2})) - .then(_assert('set cmid=2', { - cauto: true, - cmid: 2, - cmin: -1, - cmax: 5 - })) - .then(_restyle({'marker.cmin': 0})) - .then(_assert('set cmin=0', { - cauto: false, - cmid: undefined, - cmin: 0, - cmax: 5 - })) - .then(_restyle({'marker.cmax': 10})) - .then(_assert('set cmin=0 + cmax=10', { - cauto: false, - cmid: undefined, - cmin: 0, - cmax: 10 - })) - .then(_restyle({'marker.cauto': true, 'marker.cmid': null})) - .then(_assert('back to cauto=true', { - cauto: true, - cmid: undefined, - cmin: -1, - cmax: 4 - })) - .then(done, done.fail); - }); - - it('turns off autobin when you edit bin specs', function(done) { + Plotly.newPlot(gd, [ + { + mode: 'markers', + y: [1, 2, 1], + marker: { color: [1, -1, 4] } + } + ]) + .then( + _assert('base', { + cauto: true, + cmid: undefined, + cmin: -1, + cmax: 4 + }) + ) + .then(_restyle({ 'marker.cmid': 0 })) + .then( + _assert('set cmid=0', { + cauto: true, + cmid: 0, + cmin: -4, + cmax: 4 + }) + ) + .then(_restyle({ 'marker.cmid': -2 })) + .then( + _assert('set cmid=-2', { + cauto: true, + cmid: -2, + cmin: -8, + cmax: 4 + }) + ) + .then(_restyle({ 'marker.cmid': 2 })) + .then( + _assert('set cmid=2', { + cauto: true, + cmid: 2, + cmin: -1, + cmax: 5 + }) + ) + .then(_restyle({ 'marker.cmin': 0 })) + .then( + _assert('set cmin=0', { + cauto: false, + cmid: undefined, + cmin: 0, + cmax: 5 + }) + ) + .then(_restyle({ 'marker.cmax': 10 })) + .then( + _assert('set cmin=0 + cmax=10', { + cauto: false, + cmid: undefined, + cmin: 0, + cmax: 10 + }) + ) + .then(_restyle({ 'marker.cauto': true, 'marker.cmid': null })) + .then( + _assert('back to cauto=true', { + cauto: true, + cmid: undefined, + cmin: -1, + cmax: 4 + }) + ) + .then(done, done.fail); + }); + + it('turns off autobin when you edit bin specs', function (done) { // test retained (modified) for backward compat with new autobin logic var start0 = 0.2; var end1 = 6; @@ -1305,15 +1413,15 @@ describe('Test plot api', function() { expect(gd.data[1].autobinx).toBeUndefined(msg); expect(gd.data[1].autobiny).toBeUndefined(msg); - if(auto) { + if (auto) { expect(gd.data[0].xbins).toBeUndefined(msg); expect(gd.data[1].xbins).toBeUndefined(msg); expect(gd.data[1].ybins).toBeUndefined(msg); } else { // we can have - and use - partial autobin now - expect(gd.data[0].xbins).toEqual({start: start0}); - expect(gd.data[1].xbins).toEqual({end: end1}); - expect(gd.data[1].ybins).toEqual({size: size1}); + expect(gd.data[0].xbins).toEqual({ start: start0 }); + expect(gd.data[1].xbins).toEqual({ end: end1 }); + expect(gd.data[1].ybins).toEqual({ size: size1 }); expect(gd._fullData[0].xbins.start).toBe(start0, msg); expect(gd._fullData[1].xbins.end).toBe(end1, msg); expect(gd._fullData[1].ybins.size).toBe(size1, msg); @@ -1321,29 +1429,37 @@ describe('Test plot api', function() { } Plotly.newPlot(gd, [ - {x: [1, 1, 1, 1, 2, 2, 2, 3, 3, 4], type: 'histogram'}, - {x: [1, 1, 2, 2, 3, 3, 4, 4], y: [1, 1, 2, 2, 3, 3, 4, 4], type: 'histogram2d'} + { x: [1, 1, 1, 1, 2, 2, 2, 3, 3, 4], type: 'histogram' }, + { x: [1, 1, 2, 2, 3, 3, 4, 4], y: [1, 1, 2, 2, 3, 3, 4, 4], type: 'histogram2d' } ]) - .then(function() { - check(true, 'initial'); - }) - .then(function() { return Plotly.restyle(gd, 'xbins.start', start0, [0]); }) - .then(function() { return Plotly.restyle(gd, {'xbins.end': end1, 'ybins.size': size1}, null, [1]); }) - .then(function() { - check(false, 'set start/end/size'); - }) - .then(function() { return Plotly.restyle(gd, {autobinx: true, autobiny: true}); }) - .then(function() { - check(true, 'reset'); - }) - .then(function() { return Queue.undo(gd); }) - .then(function() { - check(false, 'undo'); - }) - .then(done, done.fail); + .then(function () { + check(true, 'initial'); + }) + .then(function () { + return Plotly.restyle(gd, 'xbins.start', start0, [0]); + }) + .then(function () { + return Plotly.restyle(gd, { 'xbins.end': end1, 'ybins.size': size1 }, null, [1]); + }) + .then(function () { + check(false, 'set start/end/size'); + }) + .then(function () { + return Plotly.restyle(gd, { autobinx: true, autobiny: true }); + }) + .then(function () { + check(true, 'reset'); + }) + .then(function () { + return Queue.undo(gd); + }) + .then(function () { + check(false, 'undo'); + }) + .then(done, done.fail); }); - it('turns off autocontour when you edit contour specs', function(done) { + it('turns off autocontour when you edit contour specs', function (done) { var start0 = 1.7; var size1 = 0.6; @@ -1355,28 +1471,34 @@ describe('Test plot api', function() { } Plotly.newPlot(gd, [ - {z: [[1, 2], [3, 4]], type: 'contour'}, - {x: [1, 2, 3, 4], y: [3, 4, 5, 6], type: 'histogram2dcontour'} + { + z: [ + [1, 2], + [3, 4] + ], + type: 'contour' + }, + { x: [1, 2, 3, 4], y: [3, 4, 5, 6], type: 'histogram2dcontour' } ]) - .then(function() { - check(true, 'initial'); - return Plotly.restyle(gd, 'contours.start', start0, [0]); - }) - .then(function() { - return Plotly.restyle(gd, 'contours.size', size1, [1]); - }) - .then(function() { - check(false, 'set start/size'); - return Plotly.restyle(gd, 'autocontour', true); - }) - .then(function() { - check(true, 'reset'); - return Queue.undo(gd); - }) - .then(function() { - check(false, 'undo'); - }) - .then(done, done.fail); + .then(function () { + check(true, 'initial'); + return Plotly.restyle(gd, 'contours.start', start0, [0]); + }) + .then(function () { + return Plotly.restyle(gd, 'contours.size', size1, [1]); + }) + .then(function () { + check(false, 'set start/size'); + return Plotly.restyle(gd, 'autocontour', true); + }) + .then(function () { + check(true, 'reset'); + return Queue.undo(gd); + }) + .then(function () { + check(false, 'undo'); + }) + .then(done, done.fail); }); function checkScaling(xyType, xyTypeIn, iIn, iOut) { @@ -1386,24 +1508,32 @@ describe('Test plot api', function() { expect(gd.data[iIn].ytype).toBe(xyTypeIn); } - it('sets heatmap xtype/ytype when you edit x/y data or scaling params', function(done) { - Plotly.newPlot(gd, [{type: 'heatmap', z: [[0, 1], [2, 3]]}]) - .then(function() { - // TODO would probably be better to actively default to 'array' here... - checkScaling(undefined, undefined, 0, 0); - return Plotly.restyle(gd, {x: [[2, 4]], y: [[3, 5]]}, [0]); - }) - .then(function() { - checkScaling('array', 'array', 0, 0); - return Plotly.restyle(gd, {x0: 1, dy: 3}, [0]); - }) - .then(function() { - checkScaling('scaled', 'scaled', 0, 0); - }) - .then(done, done.fail); + it('sets heatmap xtype/ytype when you edit x/y data or scaling params', function (done) { + Plotly.newPlot(gd, [ + { + type: 'heatmap', + z: [ + [0, 1], + [2, 3] + ] + } + ]) + .then(function () { + // TODO would probably be better to actively default to 'array' here... + checkScaling(undefined, undefined, 0, 0); + return Plotly.restyle(gd, { x: [[2, 4]], y: [[3, 5]] }, [0]); + }) + .then(function () { + checkScaling('array', 'array', 0, 0); + return Plotly.restyle(gd, { x0: 1, dy: 3 }, [0]); + }) + .then(function () { + checkScaling('scaled', 'scaled', 0, 0); + }) + .then(done, done.fail); }); - it('sets colorbar.tickmode to linear when editing colorbar.tick0/dtick', function(done) { + it('sets colorbar.tickmode to linear when editing colorbar.tick0/dtick', function (done) { // note: this *should* apply to marker.colorbar etc too but currently that's not implemented // once we get this all in the schema it will work though. var tick00 = 0.33; @@ -1417,204 +1547,212 @@ describe('Test plot api', function() { } Plotly.newPlot(gd, [ - {z: [[1, 2], [3, 4]], type: 'heatmap'}, - {x: [2, 3], z: [[1, 2], [3, 4]], type: 'heatmap'} + { + z: [ + [1, 2], + [3, 4] + ], + type: 'heatmap' + }, + { + x: [2, 3], + z: [ + [1, 2], + [3, 4] + ], + type: 'heatmap' + } ]) - .then(function() { - check(true, 'initial'); - return Plotly.restyle(gd, 'colorbar.tick0', tick00, [0]); - }) - .then(function() { - return Plotly.restyle(gd, 'colorbar.dtick', dtick1, [1]); - }) - .then(function() { - check(false, 'change tick0, dtick'); - return Plotly.restyle(gd, 'colorbar.tickmode', 'auto'); - }) - .then(function() { - check(true, 'reset'); - return Queue.undo(gd); - }) - .then(function() { - check(false, 'undo'); - }) - .then(done, done.fail); + .then(function () { + check(true, 'initial'); + return Plotly.restyle(gd, 'colorbar.tick0', tick00, [0]); + }) + .then(function () { + return Plotly.restyle(gd, 'colorbar.dtick', dtick1, [1]); + }) + .then(function () { + check(false, 'change tick0, dtick'); + return Plotly.restyle(gd, 'colorbar.tickmode', 'auto'); + }) + .then(function () { + check(true, 'reset'); + return Queue.undo(gd); + }) + .then(function () { + check(false, 'undo'); + }) + .then(done, done.fail); }); - it('updates colorbars when editing bar charts', function(done) { + it('updates colorbars when editing bar charts', function (done) { var mock = require('../../image/mocks/bar-colorscale-colorbar.json'); Plotly.newPlot(gd, mock.data, mock.layout) - .then(function() { - expect(d3Select('.cbaxis text').node().style.fill).not.toBe('rgb(255, 0, 0)'); + .then(function () { + expect(d3Select('.cbaxis text').node().style.fill).not.toBe('rgb(255, 0, 0)'); - return Plotly.restyle(gd, {'marker.colorbar.tickfont.color': 'rgb(255, 0, 0)'}); - }) - .then(function() { - expect(d3Select('.cbaxis text').node().style.fill).toBe('rgb(255, 0, 0)'); + return Plotly.restyle(gd, { 'marker.colorbar.tickfont.color': 'rgb(255, 0, 0)' }); + }) + .then(function () { + expect(d3Select('.cbaxis text').node().style.fill).toBe('rgb(255, 0, 0)'); - return Plotly.restyle(gd, {'marker.showscale': false}); - }) - .then(function() { - expect(d3Select('.cbaxis').size()).toBe(0); - }) - .then(done, done.fail); + return Plotly.restyle(gd, { 'marker.showscale': false }); + }) + .then(function () { + expect(d3Select('.cbaxis').size()).toBe(0); + }) + .then(done, done.fail); }); - it('updates colorbars when editing gl3d plots', function(done) { - Plotly.newPlot(gd, [{z: [[1, 2], [3, 6]], type: 'surface'}]) - .then(function() { - expect(d3Select('.cbaxis text').node().style.fill).not.toBe('rgb(255, 0, 0)'); + it('updates colorbars when editing gl3d plots', function (done) { + Plotly.newPlot(gd, [ + { + z: [ + [1, 2], + [3, 6] + ], + type: 'surface' + } + ]) + .then(function () { + expect(d3Select('.cbaxis text').node().style.fill).not.toBe('rgb(255, 0, 0)'); - return Plotly.restyle(gd, {'colorbar.tickfont.color': 'rgb(255, 0, 0)'}); - }) - .then(function() { - expect(d3Select('.cbaxis text').node().style.fill).toBe('rgb(255, 0, 0)'); + return Plotly.restyle(gd, { 'colorbar.tickfont.color': 'rgb(255, 0, 0)' }); + }) + .then(function () { + expect(d3Select('.cbaxis text').node().style.fill).toBe('rgb(255, 0, 0)'); - return Plotly.restyle(gd, {showscale: false}); - }) - .then(function() { - expect(d3Select('.cbaxis').size()).toBe(0); - }) - .then(done, done.fail); + return Plotly.restyle(gd, { showscale: false }); + }) + .then(function () { + expect(d3Select('.cbaxis').size()).toBe(0); + }) + .then(done, done.fail); }); - it('updates box position and axis type when it falls back to name', function(done) { - Plotly.newPlot(gd, [{name: 'A', y: [1, 2, 3, 4, 5], type: 'box'}], - {width: 400, height: 400, xaxis: {nticks: 3}} - ) - .then(function() { - checkTicks('x', ['A'], 'initial'); - expect(gd._fullLayout.xaxis.type).toBe('category'); - - return Plotly.restyle(gd, {name: 'B'}); + it('updates box position and axis type when it falls back to name', function (done) { + Plotly.newPlot(gd, [{ name: 'A', y: [1, 2, 3, 4, 5], type: 'box' }], { + width: 400, + height: 400, + xaxis: { nticks: 3 } }) - .then(function() { - checkTicks('x', ['B'], 'changed category'); - expect(gd._fullLayout.xaxis.type).toBe('category'); + .then(function () { + checkTicks('x', ['A'], 'initial'); + expect(gd._fullLayout.xaxis.type).toBe('category'); - return Plotly.restyle(gd, {x0: 12.3}); - }) - .then(function() { - checkTicks('x', ['12', '12.5'], 'switched to numeric'); - expect(gd._fullLayout.xaxis.type).toBe('linear'); - }) - .then(done, done.fail); + return Plotly.restyle(gd, { name: 'B' }); + }) + .then(function () { + checkTicks('x', ['B'], 'changed category'); + expect(gd._fullLayout.xaxis.type).toBe('category'); + + return Plotly.restyle(gd, { x0: 12.3 }); + }) + .then(function () { + checkTicks('x', ['12', '12.5'], 'switched to numeric'); + expect(gd._fullLayout.xaxis.type).toBe('linear'); + }) + .then(done, done.fail); }); - it('updates scene axis types automatically', function(done) { - Plotly.newPlot(gd, [{x: [1, 2], y: [1, 2], z: [1, 2], type: 'scatter3d'}]) - .then(function() { - expect(gd._fullLayout.scene.xaxis.type).toBe('linear'); + it('updates scene axis types automatically', function (done) { + Plotly.newPlot(gd, [{ x: [1, 2], y: [1, 2], z: [1, 2], type: 'scatter3d' }]) + .then(function () { + expect(gd._fullLayout.scene.xaxis.type).toBe('linear'); - return Plotly.restyle(gd, {z: [['a', 'b']]}); - }) - .then(function() { - expect(gd._fullLayout.scene.zaxis.type).toBe('category'); - }) - .then(done, done.fail); + return Plotly.restyle(gd, { z: [['a', 'b']] }); + }) + .then(function () { + expect(gd._fullLayout.scene.zaxis.type).toBe('category'); + }) + .then(done, done.fail); }); - it('can drop Cartesian while constraints are active', function(done) { - Plotly.newPlot(gd, [{x: [1, 2, 3], y: [1, 3, 2], z: [2, 3, 1]}], {xaxis: {scaleanchor: 'y'}}) - .then(function() { - expect(gd._fullLayout._axisConstraintGroups).toBeDefined(); - expect(gd._fullLayout.scene !== undefined).toBe(false); - return Plotly.restyle(gd, {type: 'scatter3d'}); - }) - .then(function() { - expect(gd._fullLayout._axisConstraintGroups).toBeUndefined(); - expect(gd._fullLayout.scene !== undefined).toBe(true); - }) - .then(done, done.fail); + it('can drop Cartesian while constraints are active', function (done) { + Plotly.newPlot(gd, [{ x: [1, 2, 3], y: [1, 3, 2], z: [2, 3, 1] }], { xaxis: { scaleanchor: 'y' } }) + .then(function () { + expect(gd._fullLayout._axisConstraintGroups).toBeDefined(); + expect(gd._fullLayout.scene !== undefined).toBe(false); + return Plotly.restyle(gd, { type: 'scatter3d' }); + }) + .then(function () { + expect(gd._fullLayout._axisConstraintGroups).toBeUndefined(); + expect(gd._fullLayout.scene !== undefined).toBe(true); + }) + .then(done, done.fail); }); }); - describe('Plotly.deleteTraces', function() { + describe('Plotly.deleteTraces', function () { var gd; - beforeEach(function() { + beforeEach(function () { gd = { - data: [ - {name: 'a'}, - {name: 'b'}, - {name: 'c'}, - {name: 'd'} - ] + data: [{ name: 'a' }, { name: 'b' }, { name: 'c' }, { name: 'd' }] }; spyOn(plotApi, 'redraw'); }); - it('should throw an error when indices are omitted', function() { - expect(function() { + it('should throw an error when indices are omitted', function () { + expect(function () { Plotly.deleteTraces(gd); }).toThrow(new Error('indices must be an integer or array of integers.')); }); - it('should throw an error when indices are out of bounds', function() { - expect(function() { + it('should throw an error when indices are out of bounds', function () { + expect(function () { Plotly.deleteTraces(gd, 10); }).toThrow(new Error('indices must be valid indices for gd.data.')); }); - it('should throw an error when indices are repeated', function() { - expect(function() { + it('should throw an error when indices are repeated', function () { + expect(function () { Plotly.deleteTraces(gd, [0, 0]); }).toThrow(new Error('each index in indices must be unique.')); }); - it('should work when indices are negative', function() { - var expectedData = [ - {name: 'a'}, - {name: 'b'}, - {name: 'c'} - ]; + it('should work when indices are negative', function () { + var expectedData = [{ name: 'a' }, { name: 'b' }, { name: 'c' }]; Plotly.deleteTraces(gd, -1); expect(gd.data).toEqual(expectedData); expect(plotApi.redraw).toHaveBeenCalled(); }); - it('should work when multiple traces are deleted', function() { - var expectedData = [ - {name: 'b'}, - {name: 'c'} - ]; + it('should work when multiple traces are deleted', function () { + var expectedData = [{ name: 'b' }, { name: 'c' }]; Plotly.deleteTraces(gd, [0, 3]); expect(gd.data).toEqual(expectedData); expect(plotApi.redraw).toHaveBeenCalled(); }); - it('should work when indices are not sorted', function() { - var expectedData = [ - {name: 'b'}, - {name: 'c'} - ]; + it('should work when indices are not sorted', function () { + var expectedData = [{ name: 'b' }, { name: 'c' }]; Plotly.deleteTraces(gd, [3, 0]); expect(gd.data).toEqual(expectedData); expect(plotApi.redraw).toHaveBeenCalled(); }); - it('should work with more than 10 indices', function() { + it('should work with more than 10 indices', function () { gd.data = []; - for(var i = 0; i < 20; i++) { + for (var i = 0; i < 20; i++) { gd.data.push({ name: 'trace #' + i }); } var expectedData = [ - {name: 'trace #12'}, - {name: 'trace #13'}, - {name: 'trace #14'}, - {name: 'trace #15'}, - {name: 'trace #16'}, - {name: 'trace #17'}, - {name: 'trace #18'}, - {name: 'trace #19'} + { name: 'trace #12' }, + { name: 'trace #13' }, + { name: 'trace #14' }, + { name: 'trace #15' }, + { name: 'trace #16' }, + { name: 'trace #17' }, + { name: 'trace #18' }, + { name: 'trace #19' } ]; Plotly.deleteTraces(gd, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); @@ -1623,26 +1761,26 @@ describe('Test plot api', function() { }); }); - describe('Plotly.addTraces', function() { + describe('Plotly.addTraces', function () { var gd; - beforeEach(function() { - gd = { data: [{name: 'a'}, {name: 'b'}] }; + beforeEach(function () { + gd = { data: [{ name: 'a' }, { name: 'b' }] }; spyOn(plotApi, 'redraw'); spyOn(plotApi, 'moveTraces'); }); - it('should throw an error when traces is not an object or an array of objects', function() { + it('should throw an error when traces is not an object or an array of objects', function () { var expected = JSON.parse(JSON.stringify(gd)); - expect(function() { + expect(function () { Plotly.addTraces(gd, 1, 2); }).toThrowError(Error, 'all values in traces array must be non-array objects'); - expect(function() { + expect(function () { Plotly.addTraces(gd, [{}, 4], 2); }).toThrowError(Error, 'all values in traces array must be non-array objects'); - expect(function() { + expect(function () { Plotly.addTraces(gd, [{}, []], 2); }).toThrowError(Error, 'all values in traces array must be non-array objects'); @@ -1650,16 +1788,16 @@ describe('Test plot api', function() { expect(gd).toEqual(expected); }); - it('should throw an error when traces and newIndices arrays are unequal', function() { - expect(function() { + it('should throw an error when traces and newIndices arrays are unequal', function () { + expect(function () { Plotly.addTraces(gd, [{}, {}], 2); }).toThrowError(Error, 'if indices is specified, traces.length must equal indices.length'); }); - it('should throw an error when newIndices are out of bounds', function() { + it('should throw an error when newIndices are out of bounds', function () { var expected = JSON.parse(JSON.stringify(gd)); - expect(function() { + expect(function () { Plotly.addTraces(gd, [{}, {}], [0, 10]); }).toThrow(new Error('newIndices must be valid indices for gd.data.')); @@ -1667,45 +1805,45 @@ describe('Test plot api', function() { expect(gd).toEqual(expected); }); - it('should work when newIndices is undefined', function() { - Plotly.addTraces(gd, [{name: 'c'}, {name: 'd'}]); + it('should work when newIndices is undefined', function () { + Plotly.addTraces(gd, [{ name: 'c' }, { name: 'd' }]); expect(gd.data[2].name).toBeDefined(); expect(gd.data[3].name).toBeDefined(); expect(plotApi.redraw).toHaveBeenCalled(); expect(plotApi.moveTraces).not.toHaveBeenCalled(); }); - it('should work when newIndices is defined', function() { - Plotly.addTraces(gd, [{name: 'c'}, {name: 'd'}], [1, 3]); + it('should work when newIndices is defined', function () { + Plotly.addTraces(gd, [{ name: 'c' }, { name: 'd' }], [1, 3]); expect(gd.data[2].name).toBeDefined(); expect(gd.data[3].name).toBeDefined(); expect(plotApi.redraw).not.toHaveBeenCalled(); expect(plotApi.moveTraces).toHaveBeenCalledWith(gd, [-2, -1], [1, 3]); }); - it('should work when newIndices has negative indices', function() { - Plotly.addTraces(gd, [{name: 'c'}, {name: 'd'}], [-3, -1]); + it('should work when newIndices has negative indices', function () { + Plotly.addTraces(gd, [{ name: 'c' }, { name: 'd' }], [-3, -1]); expect(gd.data[2].name).toBeDefined(); expect(gd.data[3].name).toBeDefined(); expect(plotApi.redraw).not.toHaveBeenCalled(); expect(plotApi.moveTraces).toHaveBeenCalledWith(gd, [-2, -1], [-3, -1]); }); - it('should work when newIndices is an integer', function() { - Plotly.addTraces(gd, {name: 'c'}, 0); + it('should work when newIndices is an integer', function () { + Plotly.addTraces(gd, { name: 'c' }, 0); expect(gd.data[2].name).toBeDefined(); expect(plotApi.redraw).not.toHaveBeenCalled(); expect(plotApi.moveTraces).toHaveBeenCalledWith(gd, [-1], [0]); }); - it('should work when adding an existing trace', function() { + it('should work when adding an existing trace', function () { Plotly.addTraces(gd, gd.data[0]); expect(gd.data.length).toEqual(3); expect(gd.data[0]).not.toBe(gd.data[2]); }); - it('should work when duplicating the existing data', function() { + it('should work when duplicating the existing data', function () { Plotly.addTraces(gd, gd.data); expect(gd.data.length).toEqual(4); @@ -1714,121 +1852,96 @@ describe('Test plot api', function() { }); }); - describe('Plotly.moveTraces should', function() { + describe('Plotly.moveTraces should', function () { var gd; - beforeEach(function() { + beforeEach(function () { gd = { - data: [ - {name: 'a'}, - {name: 'b'}, - {name: 'c'}, - {name: 'd'} - ] + data: [{ name: 'a' }, { name: 'b' }, { name: 'c' }, { name: 'd' }] }; spyOn(plotApi, 'redraw'); }); - it('throw an error when index arrays are unequal', function() { - expect(function() { + it('throw an error when index arrays are unequal', function () { + expect(function () { Plotly.moveTraces(gd, [1], [2, 1]); }).toThrow(new Error('current and new indices must be of equal length.')); }); - it('throw an error when gd.data isn\'t an array.', function() { - expect(function() { + it("throw an error when gd.data isn't an array.", function () { + expect(function () { Plotly.moveTraces({}, [0], [0]); }).toThrow(new Error('gd.data must be an array.')); - expect(function() { - Plotly.moveTraces({data: 'meow'}, [0], [0]); + expect(function () { + Plotly.moveTraces({ data: 'meow' }, [0], [0]); }).toThrow(new Error('gd.data must be an array.')); }); - it('thow an error when a current index is out of bounds', function() { - expect(function() { + it('thow an error when a current index is out of bounds', function () { + expect(function () { Plotly.moveTraces(gd, [-gd.data.length - 1], [0]); }).toThrow(new Error('currentIndices must be valid indices for gd.data.')); - expect(function() { + expect(function () { Plotly.moveTraces(gd, [gd.data.length], [0]); }).toThrow(new Error('currentIndices must be valid indices for gd.data.')); }); - it('thow an error when a new index is out of bounds', function() { - expect(function() { + it('thow an error when a new index is out of bounds', function () { + expect(function () { Plotly.moveTraces(gd, [0], [-gd.data.length - 1]); }).toThrow(new Error('newIndices must be valid indices for gd.data.')); - expect(function() { + expect(function () { Plotly.moveTraces(gd, [0], [gd.data.length]); }).toThrow(new Error('newIndices must be valid indices for gd.data.')); }); - it('thow an error when current indices are repeated', function() { - expect(function() { + it('thow an error when current indices are repeated', function () { + expect(function () { Plotly.moveTraces(gd, [0, 0], [0, 1]); }).toThrow(new Error('each index in currentIndices must be unique.')); // note that both positive and negative indices are accepted! - expect(function() { + expect(function () { Plotly.moveTraces(gd, [0, -gd.data.length], [0, 1]); }).toThrow(new Error('each index in currentIndices must be unique.')); }); - it('thow an error when new indices are repeated', function() { - expect(function() { + it('thow an error when new indices are repeated', function () { + expect(function () { Plotly.moveTraces(gd, [0, 1], [0, 0]); }).toThrow(new Error('each index in newIndices must be unique.')); // note that both positive and negative indices are accepted! - expect(function() { + expect(function () { Plotly.moveTraces(gd, [0, 1], [-gd.data.length, 0]); }).toThrow(new Error('each index in newIndices must be unique.')); }); - it('accept integers in place of arrays', function() { - var expectedData = [ - {name: 'b'}, - {name: 'a'}, - {name: 'c'}, - {name: 'd'} - ]; + it('accept integers in place of arrays', function () { + var expectedData = [{ name: 'b' }, { name: 'a' }, { name: 'c' }, { name: 'd' }]; Plotly.moveTraces(gd, 0, 1); expect(gd.data).toEqual(expectedData); expect(plotApi.redraw).toHaveBeenCalled(); }); - it('handle unsorted currentIndices', function() { - var expectedData = [ - {name: 'd'}, - {name: 'a'}, - {name: 'c'}, - {name: 'b'} - ]; + it('handle unsorted currentIndices', function () { + var expectedData = [{ name: 'd' }, { name: 'a' }, { name: 'c' }, { name: 'b' }]; Plotly.moveTraces(gd, [3, 1], [0, 3]); expect(gd.data).toEqual(expectedData); expect(plotApi.redraw).toHaveBeenCalled(); }); - it('work when newIndices are undefined.', function() { - var expectedData = [ - {name: 'b'}, - {name: 'c'}, - {name: 'd'}, - {name: 'a'} - ]; + it('work when newIndices are undefined.', function () { + var expectedData = [{ name: 'b' }, { name: 'c' }, { name: 'd' }, { name: 'a' }]; Plotly.moveTraces(gd, [3, 0]); expect(gd.data).toEqual(expectedData); expect(plotApi.redraw).toHaveBeenCalled(); }); - it('accept negative indices.', function() { - var expectedData = [ - {name: 'a'}, - {name: 'c'}, - {name: 'b'}, - {name: 'd'} - ]; + it('accept negative indices.', function () { + var expectedData = [{ name: 'a' }, { name: 'c' }, { name: 'b' }, { name: 'd' }]; Plotly.moveTraces(gd, 1, -2); expect(gd.data).toEqual(expectedData); @@ -1836,14 +1949,14 @@ describe('Test plot api', function() { }); }); - describe('Plotly.extendTraces / Plotly.prependTraces', function() { + describe('Plotly.extendTraces / Plotly.prependTraces', function () { var gd; - beforeEach(function() { + beforeEach(function () { gd = { data: [ - {x: [0, 1, 2], marker: {size: [3, 2, 1]}}, - {x: [1, 2, 3], marker: {size: [2, 3, 4]}} + { x: [0, 1, 2], marker: { size: [3, 2, 1] } }, + { x: [1, 2, 3], marker: { size: [2, 3, 4] } } ] }; @@ -1851,148 +1964,246 @@ describe('Test plot api', function() { spyOn(Queue, 'add'); }); - it('should throw an error when gd.data isn\'t an array.', function() { - expect(function() { - Plotly.extendTraces({}, {x: [[1]]}, [0]); + it("should throw an error when gd.data isn't an array.", function () { + expect(function () { + Plotly.extendTraces({}, { x: [[1]] }, [0]); }).toThrow(new Error('gd.data must be an array')); - expect(function() { - Plotly.extendTraces({data: 'meow'}, {x: [[1]]}, [0]); + expect(function () { + Plotly.extendTraces({ data: 'meow' }, { x: [[1]] }, [0]); }).toThrow(new Error('gd.data must be an array')); }); - it('should throw an error when update is not an object', function() { - expect(function() { + it('should throw an error when update is not an object', function () { + expect(function () { Plotly.extendTraces(gd, undefined, [0], 8); }).toThrow(new Error('update must be a key:value object')); - expect(function() { + expect(function () { Plotly.extendTraces(gd, null, [0]); }).toThrow(new Error('update must be a key:value object')); }); - it('should throw an error when indices are omitted', function() { - expect(function() { - Plotly.extendTraces(gd, {x: [[1]]}); + it('should throw an error when indices are omitted', function () { + expect(function () { + Plotly.extendTraces(gd, { x: [[1]] }); }).toThrow(new Error('indices must be an integer or array of integers')); }); - it('should throw an error when a current index is out of bounds', function() { - expect(function() { - Plotly.extendTraces(gd, {x: [[1]]}, [-gd.data.length - 1]); + it('should throw an error when a current index is out of bounds', function () { + expect(function () { + Plotly.extendTraces(gd, { x: [[1]] }, [-gd.data.length - 1]); }).toThrow(new Error('indices must be valid indices for gd.data.')); }); - it('should not throw an error when negative index wraps to positive', function() { - expect(function() { - Plotly.extendTraces(gd, {x: [[1]]}, [-1]); + it('should not throw an error when negative index wraps to positive', function () { + expect(function () { + Plotly.extendTraces(gd, { x: [[1]] }, [-1]); }).not.toThrow(); }); - it('should throw an error when number of Indices does not match Update arrays', function() { - expect(function() { - Plotly.extendTraces(gd, {x: [[1, 2], [2, 3]] }, [0]); + it('should throw an error when number of Indices does not match Update arrays', function () { + expect(function () { + Plotly.extendTraces( + gd, + { + x: [ + [1, 2], + [2, 3] + ] + }, + [0] + ); }).toThrow(new Error('attribute x must be an array of length equal to indices array length')); - expect(function() { - Plotly.extendTraces(gd, {x: [[1]]}, [0, 1]); + expect(function () { + Plotly.extendTraces(gd, { x: [[1]] }, [0, 1]); }).toThrow(new Error('attribute x must be an array of length equal to indices array length')); }); - it('should throw an error when maxPoints is an Object but does not match Update', function() { - expect(function() { - Plotly.extendTraces(gd, {x: [[1]]}, [0], {y: [1]}); - }).toThrow(new Error('when maxPoints is set as a key:value object it must contain a 1:1 ' + - 'correspondence with the keys and number of traces in the update object')); + it('should throw an error when maxPoints is an Object but does not match Update', function () { + expect(function () { + Plotly.extendTraces(gd, { x: [[1]] }, [0], { y: [1] }); + }).toThrow( + new Error( + 'when maxPoints is set as a key:value object it must contain a 1:1 ' + + 'correspondence with the keys and number of traces in the update object' + ) + ); - expect(function() { - Plotly.extendTraces(gd, {x: [[1]]}, [0], {x: [1, 2]}); - }).toThrow(new Error('when maxPoints is set as a key:value object it must contain a 1:1 ' + - 'correspondence with the keys and number of traces in the update object')); + expect(function () { + Plotly.extendTraces(gd, { x: [[1]] }, [0], { x: [1, 2] }); + }).toThrow( + new Error( + 'when maxPoints is set as a key:value object it must contain a 1:1 ' + + 'correspondence with the keys and number of traces in the update object' + ) + ); }); - it('should throw an error when update keys mismatch trace keys', function() { + it('should throw an error when update keys mismatch trace keys', function () { // lets update y on both traces, but only 1 trace has "y" gd.data[1].y = [1, 2, 3]; - expect(function() { - Plotly.extendTraces(gd, { - y: [[3, 4], [4, 5]], 'marker.size': [[0, -1], [5, 6]] - }, [0, 1]); + expect(function () { + Plotly.extendTraces( + gd, + { + y: [ + [3, 4], + [4, 5] + ], + 'marker.size': [ + [0, -1], + [5, 6] + ] + }, + [0, 1] + ); }).toThrow(new Error('cannot extend missing or non-array attribute: y')); }); - it('should extend traces with update keys', function() { - Plotly.extendTraces(gd, { - x: [[3, 4], [4, 5]], 'marker.size': [[0, -1], [5, 6]] - }, [0, 1]); + it('should extend traces with update keys', function () { + Plotly.extendTraces( + gd, + { + x: [ + [3, 4], + [4, 5] + ], + 'marker.size': [ + [0, -1], + [5, 6] + ] + }, + [0, 1] + ); expect(gd.data).toEqual([ - {x: [0, 1, 2, 3, 4], marker: {size: [3, 2, 1, 0, -1]}}, - {x: [1, 2, 3, 4, 5], marker: {size: [2, 3, 4, 5, 6]}} + { x: [0, 1, 2, 3, 4], marker: { size: [3, 2, 1, 0, -1] } }, + { x: [1, 2, 3, 4, 5], marker: { size: [2, 3, 4, 5, 6] } } ]); expect(plotApi.redraw).toHaveBeenCalled(); }); - it('should extend and window traces with update keys', function() { + it('should extend and window traces with update keys', function () { var maxPoints = 3; - Plotly.extendTraces(gd, { - x: [[3, 4], [4, 5]], 'marker.size': [[0, -1], [5, 6]] - }, [0, 1], maxPoints); + Plotly.extendTraces( + gd, + { + x: [ + [3, 4], + [4, 5] + ], + 'marker.size': [ + [0, -1], + [5, 6] + ] + }, + [0, 1], + maxPoints + ); expect(gd.data).toEqual([ - {x: [2, 3, 4], marker: {size: [1, 0, -1]}}, - {x: [3, 4, 5], marker: {size: [4, 5, 6]}} + { x: [2, 3, 4], marker: { size: [1, 0, -1] } }, + { x: [3, 4, 5], marker: { size: [4, 5, 6] } } ]); }); - it('should extend and window traces with update keys', function() { + it('should extend and window traces with update keys', function () { var maxPoints = 3; - Plotly.extendTraces(gd, { - x: [[3, 4], [4, 5]], 'marker.size': [[0, -1], [5, 6]] - }, [0, 1], maxPoints); + Plotly.extendTraces( + gd, + { + x: [ + [3, 4], + [4, 5] + ], + 'marker.size': [ + [0, -1], + [5, 6] + ] + }, + [0, 1], + maxPoints + ); expect(gd.data).toEqual([ - {x: [2, 3, 4], marker: {size: [1, 0, -1]}}, - {x: [3, 4, 5], marker: {size: [4, 5, 6]}} + { x: [2, 3, 4], marker: { size: [1, 0, -1] } }, + { x: [3, 4, 5], marker: { size: [4, 5, 6] } } ]); }); - it('should extend and window traces using full maxPoint object', function() { - var maxPoints = {x: [2, 3], 'marker.size': [1, 2]}; - - Plotly.extendTraces(gd, { - x: [[3, 4], [4, 5]], 'marker.size': [[0, -1], [5, 6]] - }, [0, 1], maxPoints); + it('should extend and window traces using full maxPoint object', function () { + var maxPoints = { x: [2, 3], 'marker.size': [1, 2] }; + + Plotly.extendTraces( + gd, + { + x: [ + [3, 4], + [4, 5] + ], + 'marker.size': [ + [0, -1], + [5, 6] + ] + }, + [0, 1], + maxPoints + ); expect(gd.data).toEqual([ - {x: [3, 4], marker: {size: [-1]}}, - {x: [3, 4, 5], marker: {size: [5, 6]}} + { x: [3, 4], marker: { size: [-1] } }, + { x: [3, 4, 5], marker: { size: [5, 6] } } ]); }); - it('should truncate arrays when maxPoints is zero', function() { - Plotly.extendTraces(gd, { - x: [[3, 4], [4, 5]], 'marker.size': [[0, -1], [5, 6]] - }, [0, 1], 0); + it('should truncate arrays when maxPoints is zero', function () { + Plotly.extendTraces( + gd, + { + x: [ + [3, 4], + [4, 5] + ], + 'marker.size': [ + [0, -1], + [5, 6] + ] + }, + [0, 1], + 0 + ); expect(gd.data).toEqual([ - {x: [], marker: {size: []}}, - {x: [], marker: {size: []}} + { x: [], marker: { size: [] } }, + { x: [], marker: { size: [] } } ]); expect(plotApi.redraw).toHaveBeenCalled(); }); - it('prepend is the inverse of extend - no maxPoints', function() { + it('prepend is the inverse of extend - no maxPoints', function () { var cachedData = Lib.extendDeep([], gd.data); - Plotly.extendTraces(gd, { - x: [[3, 4], [4, 5]], 'marker.size': [[0, -1], [5, 6]] - }, [0, 1]); + Plotly.extendTraces( + gd, + { + x: [ + [3, 4], + [4, 5] + ], + 'marker.size': [ + [0, -1], + [5, 6] + ] + }, + [0, 1] + ); expect(gd.data).not.toEqual(cachedData); expect(Queue.add).toHaveBeenCalled(); @@ -2004,12 +2215,23 @@ describe('Test plot api', function() { expect(gd.data).toEqual(cachedData); }); - it('extend is the inverse of prepend - no maxPoints', function() { + it('extend is the inverse of prepend - no maxPoints', function () { var cachedData = Lib.extendDeep([], gd.data); - Plotly.prependTraces(gd, { - x: [[3, 4], [4, 5]], 'marker.size': [[0, -1], [5, 6]] - }, [0, 1]); + Plotly.prependTraces( + gd, + { + x: [ + [3, 4], + [4, 5] + ], + 'marker.size': [ + [0, -1], + [5, 6] + ] + }, + [0, 1] + ); expect(gd.data).not.toEqual(cachedData); expect(Queue.add).toHaveBeenCalled(); @@ -2021,13 +2243,25 @@ describe('Test plot api', function() { expect(gd.data).toEqual(cachedData); }); - it('prepend is the inverse of extend - with maxPoints', function() { + it('prepend is the inverse of extend - with maxPoints', function () { var maxPoints = 3; var cachedData = Lib.extendDeep([], gd.data); - Plotly.extendTraces(gd, { - x: [[3, 4], [4, 5]], 'marker.size': [[0, -1], [5, 6]] - }, [0, 1], maxPoints); + Plotly.extendTraces( + gd, + { + x: [ + [3, 4], + [4, 5] + ], + 'marker.size': [ + [0, -1], + [5, 6] + ] + }, + [0, 1], + maxPoints + ); expect(gd.data).not.toEqual(cachedData); expect(Queue.add).toHaveBeenCalled(); @@ -2039,54 +2273,72 @@ describe('Test plot api', function() { expect(gd.data).toEqual(cachedData); }); - it('should throw when trying to extend a plain array with a typed array', function() { - gd.data = [{ - x: new Float32Array([1, 2, 3]), - marker: {size: new Float32Array([20, 30, 10])} - }]; + it('should throw when trying to extend a plain array with a typed array', function () { + gd.data = [ + { + x: new Float32Array([1, 2, 3]), + marker: { size: new Float32Array([20, 30, 10]) } + } + ]; - expect(function() { - Plotly.extendTraces(gd, {x: [[1]]}, [0]); + expect(function () { + Plotly.extendTraces(gd, { x: [[1]] }, [0]); }).toThrow(new Error('cannot extend array with an array of a different type: x')); }); - it('should throw when trying to extend a typed array with a plain array', function() { - gd.data = [{ - x: [1, 2, 3], - marker: {size: [20, 30, 10]} - }]; + it('should throw when trying to extend a typed array with a plain array', function () { + gd.data = [ + { + x: [1, 2, 3], + marker: { size: [20, 30, 10] } + } + ]; - expect(function() { - Plotly.extendTraces(gd, {x: [new Float32Array([1])]}, [0]); + expect(function () { + Plotly.extendTraces(gd, { x: [new Float32Array([1])] }, [0]); }).toThrow(new Error('cannot extend array with an array of a different type: x')); }); - it('should extend traces with update keys (typed array case)', function() { - gd.data = [{ - x: new Float32Array([1, 2, 3]), - marker: {size: new Float32Array([20, 30, 10])} - }]; + it('should extend traces with update keys (typed array case)', function () { + gd.data = [ + { + x: new Float32Array([1, 2, 3]), + marker: { size: new Float32Array([20, 30, 10]) } + } + ]; - Plotly.extendTraces(gd, { - x: [new Float32Array([4, 5])], - 'marker.size': [new Float32Array([40, 30])] - }, [0]); + Plotly.extendTraces( + gd, + { + x: [new Float32Array([4, 5])], + 'marker.size': [new Float32Array([40, 30])] + }, + [0] + ); expect(gd.data[0].x).toEqual(new Float32Array([1, 2, 3, 4, 5])); expect(gd.data[0].marker.size).toEqual(new Float32Array([20, 30, 10, 40, 30])); }); - describe('should extend/prepend and window traces with update keys linked', function() { + describe('should extend/prepend and window traces with update keys linked', function () { function _base(method, args, expectations) { - gd.data = [{ - x: [1, 2, 3] - }, { - x: new Float32Array([1, 2, 3]) - }]; + gd.data = [ + { + x: [1, 2, 3] + }, + { + x: new Float32Array([1, 2, 3]) + } + ]; - Plotly[method](gd, { - x: [args.newPts, new Float32Array(args.newPts)] - }, [0, 1], args.maxp); + Plotly[method]( + gd, + { + x: [args.newPts, new Float32Array(args.newPts)] + }, + [0, 1], + args.maxp + ); expect(plotApi.redraw).toHaveBeenCalled(); expect(Queue.add).toHaveBeenCalled(); @@ -2107,278 +2359,330 @@ describe('Test plot api', function() { return _base('prependTraces', args, expectations); } - it('- extend no maxp', function() { - _extendTraces({ - newPts: [4, 5] - }, { - newArray: [1, 2, 3, 4, 5], - remainder: [] - }); + it('- extend no maxp', function () { + _extendTraces( + { + newPts: [4, 5] + }, + { + newArray: [1, 2, 3, 4, 5], + remainder: [] + } + ); }); - it('- extend maxp === insert.length', function() { - _extendTraces({ - newPts: [4, 5], - maxp: 2 - }, { - newArray: [4, 5], - remainder: [1, 2, 3] - }); + it('- extend maxp === insert.length', function () { + _extendTraces( + { + newPts: [4, 5], + maxp: 2 + }, + { + newArray: [4, 5], + remainder: [1, 2, 3] + } + ); }); - it('- extend maxp < insert.length', function() { - _extendTraces({ - newPts: [4, 5], - maxp: 1 - }, { - newArray: [5], - remainder: [1, 2, 3, 4] - }); + it('- extend maxp < insert.length', function () { + _extendTraces( + { + newPts: [4, 5], + maxp: 1 + }, + { + newArray: [5], + remainder: [1, 2, 3, 4] + } + ); }); - it('- extend maxp > insert.length', function() { - _extendTraces({ - newPts: [4, 5], - maxp: 4 - }, { - newArray: [2, 3, 4, 5], - remainder: [1] - }); + it('- extend maxp > insert.length', function () { + _extendTraces( + { + newPts: [4, 5], + maxp: 4 + }, + { + newArray: [2, 3, 4, 5], + remainder: [1] + } + ); }); - it('- extend maxp === 0', function() { - _extendTraces({ - newPts: [4, 5], - maxp: 0 - }, { - newArray: [], - remainder: [1, 2, 3, 4, 5] - }); + it('- extend maxp === 0', function () { + _extendTraces( + { + newPts: [4, 5], + maxp: 0 + }, + { + newArray: [], + remainder: [1, 2, 3, 4, 5] + } + ); }); - it('- prepend no maxp', function() { - _prependTraces({ - newPts: [-1, 0] - }, { - newArray: [-1, 0, 1, 2, 3], - remainder: [] - }); + it('- prepend no maxp', function () { + _prependTraces( + { + newPts: [-1, 0] + }, + { + newArray: [-1, 0, 1, 2, 3], + remainder: [] + } + ); }); - it('- prepend maxp === insert.length', function() { - _prependTraces({ - newPts: [-1, 0], - maxp: 2 - }, { - newArray: [-1, 0], - remainder: [1, 2, 3] - }); + it('- prepend maxp === insert.length', function () { + _prependTraces( + { + newPts: [-1, 0], + maxp: 2 + }, + { + newArray: [-1, 0], + remainder: [1, 2, 3] + } + ); }); - it('- prepend maxp < insert.length', function() { - _prependTraces({ - newPts: [-1, 0], - maxp: 1 - }, { - newArray: [-1], - remainder: [0, 1, 2, 3] - }); + it('- prepend maxp < insert.length', function () { + _prependTraces( + { + newPts: [-1, 0], + maxp: 1 + }, + { + newArray: [-1], + remainder: [0, 1, 2, 3] + } + ); }); - it('- prepend maxp > insert.length', function() { - _prependTraces({ - newPts: [-1, 0], - maxp: 4 - }, { - newArray: [-1, 0, 1, 2], - remainder: [3] - }); + it('- prepend maxp > insert.length', function () { + _prependTraces( + { + newPts: [-1, 0], + maxp: 4 + }, + { + newArray: [-1, 0, 1, 2], + remainder: [3] + } + ); }); - it('- prepend maxp === 0', function() { - _prependTraces({ - newPts: [-1, 0], - maxp: 0 - }, { - newArray: [], - remainder: [-1, 0, 1, 2, 3] - }); + it('- prepend maxp === 0', function () { + _prependTraces( + { + newPts: [-1, 0], + maxp: 0 + }, + { + newArray: [], + remainder: [-1, 0, 1, 2, 3] + } + ); }); }); }); - describe('Plotly.purge', function() { + describe('Plotly.purge', function () { afterEach(destroyGraphDiv); - it('should return the graph div in its original state', function(done) { + it('should return the graph div in its original state', function (done) { var gd = createGraphDiv(); var initialKeys = Object.keys(gd); var intialHTML = gd.innerHTML; var mockData = [{ x: [1, 2, 3], y: [2, 3, 4] }]; - Plotly.newPlot(gd, mockData).then(function() { - Plotly.purge(gd); + Plotly.newPlot(gd, mockData) + .then(function () { + Plotly.purge(gd); - expect(Object.keys(gd)).toEqual(initialKeys); - expect(gd.innerHTML).toEqual(intialHTML); - }) - .then(done, done.fail); + expect(Object.keys(gd)).toEqual(initialKeys); + expect(gd.innerHTML).toEqual(intialHTML); + }) + .then(done, done.fail); }); }); - describe('Plotly.redraw', function() { + describe('Plotly.redraw', function () { afterEach(destroyGraphDiv); - it('', function(done) { + it('', function (done) { var gd = createGraphDiv(); var initialData = []; var layout = { title: { text: 'Redraw' } }; Plotly.newPlot(gd, initialData, layout) - .then(function() { - var trace1 = { - x: [1, 2, 3, 4], - y: [4, 1, 5, 3], - name: 'First Trace' - }; - var trace2 = { - x: [1, 2, 3, 4], - y: [14, 11, 15, 13], - name: 'Second Trace' - }; - var trace3 = { - x: [1, 2, 3, 4], - y: [5, 3, 7, 1], - name: 'Third Trace' - }; - - var newData = [trace1, trace2, trace3]; - gd.data = newData; - - return Plotly.redraw(gd); - }) - .then(function() { - expect(d3SelectAll('g.trace.scatter').size()).toEqual(3); - }) - .then(done, done.fail); + .then(function () { + var trace1 = { + x: [1, 2, 3, 4], + y: [4, 1, 5, 3], + name: 'First Trace' + }; + var trace2 = { + x: [1, 2, 3, 4], + y: [14, 11, 15, 13], + name: 'Second Trace' + }; + var trace3 = { + x: [1, 2, 3, 4], + y: [5, 3, 7, 1], + name: 'Third Trace' + }; + + var newData = [trace1, trace2, trace3]; + gd.data = newData; + + return Plotly.redraw(gd); + }) + .then(function () { + expect(d3SelectAll('g.trace.scatter').size()).toEqual(3); + }) + .then(done, done.fail); }); }); - describe('cleanData & cleanLayout', function() { + describe('cleanData & cleanLayout', function () { var gd; - beforeEach(function() { + beforeEach(function () { gd = createGraphDiv(); }); afterEach(destroyGraphDiv); - it('should rename \'scl\' to \'colorscale\' when colorscale is not defined', function() { - var data = [{ - type: 'heatmap', - scl: 'Blues' - }]; + it("should rename 'scl' to 'colorscale' when colorscale is not defined", function () { + var data = [ + { + type: 'heatmap', + scl: 'Blues' + } + ]; Plotly.newPlot(gd, data); expect(gd.data[0].colorscale).toBe('Blues'); expect(gd.data[0].scl).toBe(undefined); }); - it('should not delete rename \'scl\' to \'colorscale\' when colorscale is defined ', function() { - var data = [{ - type: 'heatmap', - colorscale: 'Greens', - scl: 'Reds' - }]; + it("should not delete rename 'scl' to 'colorscale' when colorscale is defined ", function () { + var data = [ + { + type: 'heatmap', + colorscale: 'Greens', + scl: 'Reds' + } + ]; Plotly.newPlot(gd, data); expect(gd.data[0].colorscale).toBe('Greens'); expect(gd.data[0].scl).not.toBe(undefined); }); - it('should rename \'reversescl\' to \'reversescale\' when colorscale is not defined', function() { - var data = [{ - type: 'heatmap', - reversescl: true - }]; + it("should rename 'reversescl' to 'reversescale' when colorscale is not defined", function () { + var data = [ + { + type: 'heatmap', + reversescl: true + } + ]; Plotly.newPlot(gd, data); expect(gd.data[0].reversescale).toBe(true); expect(gd.data[0].reversescl).toBe(undefined); }); - it('should not delete & rename \'reversescl\' to \'reversescale\' when colorscale is defined', function() { - var data = [{ - type: 'heatmap', - reversescale: true, - reversescl: false - }]; + it("should not delete & rename 'reversescl' to 'reversescale' when colorscale is defined", function () { + var data = [ + { + type: 'heatmap', + reversescale: true, + reversescl: false + } + ]; Plotly.newPlot(gd, data); expect(gd.data[0].reversescale).toBe(true); expect(gd.data[0].reversescl).not.toBe(undefined); }); - it('should rename \'YIGnBu\' colorscales YlGnBu (2dMap case)', function() { - var data = [{ - type: 'heatmap', - colorscale: 'YIGnBu' - }]; + it("should rename 'YIGnBu' colorscales YlGnBu (2dMap case)", function () { + var data = [ + { + type: 'heatmap', + colorscale: 'YIGnBu' + } + ]; Plotly.newPlot(gd, data); expect(gd.data[0].colorscale).toBe('YlGnBu'); }); - it('should rename \'YIGnBu\' colorscales YlGnBu (markerColorscale case)', function() { - var data = [{ - type: 'scattergeo', - marker: { colorscale: 'YIGnBu' } - }]; + it("should rename 'YIGnBu' colorscales YlGnBu (markerColorscale case)", function () { + var data = [ + { + type: 'scattergeo', + marker: { colorscale: 'YIGnBu' } + } + ]; Plotly.newPlot(gd, data); expect(gd.data[0].marker.colorscale).toBe('YlGnBu'); }); - it('should rename \'YIOrRd\' colorscales YlOrRd (2dMap case)', function() { - var data = [{ - type: 'contour', - colorscale: 'YIOrRd' - }]; + it("should rename 'YIOrRd' colorscales YlOrRd (2dMap case)", function () { + var data = [ + { + type: 'contour', + colorscale: 'YIOrRd' + } + ]; Plotly.newPlot(gd, data); expect(gd.data[0].colorscale).toBe('YlOrRd'); }); - it('should rename \'YIOrRd\' colorscales YlOrRd (markerColorscale case)', function() { - var data = [{ - type: 'scattergeo', - marker: { colorscale: 'YIOrRd' } - }]; + it("should rename 'YIOrRd' colorscales YlOrRd (markerColorscale case)", function () { + var data = [ + { + type: 'scattergeo', + marker: { colorscale: 'YIOrRd' } + } + ]; Plotly.newPlot(gd, data); expect(gd.data[0].marker.colorscale).toBe('YlOrRd'); }); - it('should rename \'highlightColor\' to \'highlightcolor\')', function() { - var data = [{ - type: 'surface', - contours: { - x: { highlightColor: 'red' }, - y: { highlightcolor: 'blue' } - } - }, { - type: 'surface' - }, { - type: 'surface', - contours: false - }, { - type: 'surface', - contours: { - stuff: {}, - x: false, - y: [] + it("should rename 'highlightColor' to 'highlightcolor')", function () { + var data = [ + { + type: 'surface', + contours: { + x: { highlightColor: 'red' }, + y: { highlightcolor: 'blue' } + } + }, + { + type: 'surface' + }, + { + type: 'surface', + contours: false + }, + { + type: 'surface', + contours: { + stuff: {}, + x: false, + y: [] + } } - }]; + ]; spyOn(Plots.subplotsRegistry.gl3d, 'plot'); @@ -2398,16 +2702,19 @@ describe('Test plot api', function() { expect(gd.data[3].contours).toEqual({ stuff: {}, x: false, y: [] }); }); - it('should rename \'highlightWidth\' to \'highlightwidth\')', function() { - var data = [{ - type: 'surface', - contours: { - z: { highlightwidth: 'red' }, - y: { highlightWidth: 'blue' } + it("should rename 'highlightWidth' to 'highlightwidth')", function () { + var data = [ + { + type: 'surface', + contours: { + z: { highlightwidth: 'red' }, + y: { highlightWidth: 'blue' } + } + }, + { + type: 'surface' } - }, { - type: 'surface' - }]; + ]; spyOn(Plots.subplotsRegistry.gl3d, 'plot'); @@ -2425,19 +2732,12 @@ describe('Test plot api', function() { expect(gd.data[1].contours).toBeUndefined(); }); - it('should cleanup annotations / shapes refs', function() { + it('should cleanup annotations / shapes refs', function () { var data = [{}]; var layout = { - annotations: [ - null, - { xref: 'x02', yref: 'y1' } - ], - shapes: [ - { xref: 'y', yref: 'x' }, - null, - { xref: 'x03', yref: 'y1' } - ] + annotations: [null, { xref: 'x02', yref: 'y1' }], + shapes: [{ xref: 'y', yref: 'x' }, null, { xref: 'x03', yref: 'y1' }] }; Plotly.newPlot(gd, data, layout); @@ -2452,64 +2752,90 @@ describe('Test plot api', function() { expect(gd.layout.shapes[2].yref).toEqual('y'); }); - it('removes direction names and showlegend from finance traces', function() { - var data = [{ - type: 'ohlc', open: [1], high: [3], low: [0], close: [2], - increasing: { - showlegend: true, - name: 'Yeti goes up' + it('removes direction names and showlegend from finance traces', function () { + var data = [ + { + type: 'ohlc', + open: [1], + high: [3], + low: [0], + close: [2], + increasing: { + showlegend: true, + name: 'Yeti goes up' + }, + decreasing: { + showlegend: 'legendonly', + name: 'Yeti goes down' + }, + name: 'Snowman' }, - decreasing: { - showlegend: 'legendonly', - name: 'Yeti goes down' + { + type: 'candlestick', + open: [1], + high: [3], + low: [0], + close: [2], + increasing: { + name: 'Bigfoot' + }, + decreasing: { + showlegend: false, + name: 'Biggerfoot' + }, + name: 'Nobody' }, - name: 'Snowman' - }, { - type: 'candlestick', open: [1], high: [3], low: [0], close: [2], - increasing: { - name: 'Bigfoot' + { + type: 'ohlc', + open: [1], + high: [3], + low: [0], + close: [2], + increasing: { + name: 'Batman' + }, + decreasing: { + showlegend: true + }, + name: 'Robin' }, - decreasing: { - showlegend: false, - name: 'Biggerfoot' - }, - name: 'Nobody' - }, { - type: 'ohlc', open: [1], high: [3], low: [0], close: [2], - increasing: { - name: 'Batman' - }, - decreasing: { - showlegend: true - }, - name: 'Robin' - }, { - type: 'candlestick', open: [1], high: [3], low: [0], close: [2], - increasing: { - showlegend: false, + { + type: 'candlestick', + open: [1], + high: [3], + low: [0], + close: [2], + increasing: { + showlegend: false + }, + decreasing: { + name: 'Fred' + } }, - decreasing: { - name: 'Fred' + { + type: 'ohlc', + open: [1], + high: [3], + low: [0], + close: [2], + increasing: { + showlegend: false, + name: 'Gruyere heating up' + }, + decreasing: { + showlegend: false, + name: 'Gruyere cooling off' + }, + name: 'Emmenthaler' } - }, { - type: 'ohlc', open: [1], high: [3], low: [0], close: [2], - increasing: { - showlegend: false, - name: 'Gruyere heating up' - }, - decreasing: { - showlegend: false, - name: 'Gruyere cooling off' - }, - name: 'Emmenthaler' - }]; + ]; Plotly.newPlot(gd, data); // Even if both showlegends are false, leave trace.showlegend out // My rationale for this is that legends are sufficiently different // now that it's worthwhile resetting their existence to default - gd.data.forEach(function(trace) { + gd.data.forEach(function (trace) { expect(trace.increasing.name).toBeUndefined(); expect(trace.increasing.showlegend).toBeUndefined(); expect(trace.decreasing.name).toBeUndefined(); @@ -2537,41 +2863,44 @@ describe('Test plot api', function() { }); }); - describe('Plotly.newPlot', function() { + describe('Plotly.newPlot', function () { var gd; - beforeEach(function() { + beforeEach(function () { gd = createGraphDiv(); }); afterEach(destroyGraphDiv); - it('should respect layout.width and layout.height', function(done) { + it('should respect layout.width and layout.height', function (done) { // See issue https://github.com/plotly/plotly.js/issues/537 - var data = [{ - x: [1, 2], - y: [1, 2] - }]; + var data = [ + { + x: [1, 2], + y: [1, 2] + } + ]; var height = 50; - Plotly.newPlot(gd, data).then(function() { - return Plotly.newPlot(gd, data, { height: height }); - }) - .then(function() { - var fullLayout = gd._fullLayout; - var svg = document.getElementsByClassName('main-svg')[0]; + Plotly.newPlot(gd, data) + .then(function () { + return Plotly.newPlot(gd, data, { height: height }); + }) + .then(function () { + var fullLayout = gd._fullLayout; + var svg = document.getElementsByClassName('main-svg')[0]; - expect(fullLayout.height).toBe(height); - expect(+svg.getAttribute('height')).toBe(height); - }) - .then(done, done.fail); + expect(fullLayout.height).toBe(height); + expect(+svg.getAttribute('height')).toBe(height); + }) + .then(done, done.fail); }); - it('should only have one modebar-container', function(done) { - var data = [{y: [1, 2]}]; + it('should only have one modebar-container', function (done) { + var data = [{ y: [1, 2] }]; function _assert(msg) { - return function() { + return function () { var modebars = document.getElementsByClassName('modebar-container'); expect(modebars.length).toBe(1, msg + ' # of modebar container'); var groups = document.getElementsByClassName('modebar-group'); @@ -2580,133 +2909,150 @@ describe('Test plot api', function() { } Plotly.newPlot(gd, data) - .then(_assert('base')) - .then(function() { return Plotly.newPlot(gd, data); }) - .then(_assert('after newPlot()')) - .then(function() { - // funky combinations of update flags found in - // https://github.com/plotly/plotly.js/issues/3824 - return Plotly.update(gd, { - visible: false - }, { - annotations: [{text: 'a'}] - }); - }) - .then(_assert('after update()')) - .then(done, done.fail); + .then(_assert('base')) + .then(function () { + return Plotly.newPlot(gd, data); + }) + .then(_assert('after newPlot()')) + .then(function () { + // funky combinations of update flags found in + // https://github.com/plotly/plotly.js/issues/3824 + return Plotly.update( + gd, + { + visible: false + }, + { + annotations: [{ text: 'a' }] + } + ); + }) + .then(_assert('after update()')) + .then(done, done.fail); }); }); - describe('Plotly.update should', function() { + describe('Plotly.update should', function () { var gd, data, layout, calcdata; - beforeAll(function() { - Object.keys(subroutines).forEach(function(k) { + beforeAll(function () { + Object.keys(subroutines).forEach(function (k) { spyOn(subroutines, k).and.callThrough(); }); }); - beforeEach(function(done) { - Object.keys(subroutines).forEach(function(k) { + beforeEach(function (done) { + Object.keys(subroutines).forEach(function (k) { subroutines[k].calls.reset(); }); gd = createGraphDiv(); - Plotly.newPlot(gd, [{ y: [2, 1, 2] }]).then(function() { - data = gd.data; - layout = gd.layout; - calcdata = gd.calcdata; - }) - .then(done); + Plotly.newPlot(gd, [{ y: [2, 1, 2] }]) + .then(function () { + data = gd.data; + layout = gd.layout; + calcdata = gd.calcdata; + }) + .then(done); }); afterEach(destroyGraphDiv); - it('call doTraceStyle on trace style updates', function(done) { - Plotly.update(gd, { 'marker.color': 'blue' }).then(function() { - expect(subroutines.doTraceStyle).toHaveBeenCalledTimes(1); - expect(calcdata).toBe(gd.calcdata); - }) - .then(done, done.fail); + it('call doTraceStyle on trace style updates', function (done) { + Plotly.update(gd, { 'marker.color': 'blue' }) + .then(function () { + expect(subroutines.doTraceStyle).toHaveBeenCalledTimes(1); + expect(calcdata).toBe(gd.calcdata); + }) + .then(done, done.fail); }); - it('clear calcdata on data updates', function(done) { - Plotly.update(gd, { x: [[3, 1, 3]] }).then(function() { - expect(data).toBe(gd.data); - expect(layout).toBe(gd.layout); - expect(calcdata).not.toBe(gd.calcdata); - }) - .then(done, done.fail); + it('clear calcdata on data updates', function (done) { + Plotly.update(gd, { x: [[3, 1, 3]] }) + .then(function () { + expect(data).toBe(gd.data); + expect(layout).toBe(gd.layout); + expect(calcdata).not.toBe(gd.calcdata); + }) + .then(done, done.fail); }); - it('clear calcdata on data + axis updates w/o extending current gd.data', function(done) { + it('clear calcdata on data + axis updates w/o extending current gd.data', function (done) { var traceUpdate = { x: [[3, 1, 3]] }; var layoutUpdate = { - xaxis: { title: { text: 'A' }, type: '-'} + xaxis: { title: { text: 'A' }, type: '-' } }; - Plotly.update(gd, traceUpdate, layoutUpdate).then(function() { - expect(data).toBe(gd.data); - expect(layout).toBe(gd.layout); - expect(calcdata).not.toBe(gd.calcdata); - expect(gd.data.length).toEqual(1); - expect(subroutines.layoutReplot).toHaveBeenCalledTimes(1); - }) - .then(done, done.fail); + Plotly.update(gd, traceUpdate, layoutUpdate) + .then(function () { + expect(data).toBe(gd.data); + expect(layout).toBe(gd.layout); + expect(calcdata).not.toBe(gd.calcdata); + expect(gd.data.length).toEqual(1); + expect(subroutines.layoutReplot).toHaveBeenCalledTimes(1); + }) + .then(done, done.fail); }); - it('call doLegend on legend updates', function(done) { - Plotly.update(gd, {}, { showlegend: true }).then(function() { - expect(subroutines.doLegend).toHaveBeenCalledTimes(1); - expect(calcdata).toBe(gd.calcdata); - }) - .then(done, done.fail); + it('call doLegend on legend updates', function (done) { + Plotly.update(gd, {}, { showlegend: true }) + .then(function () { + expect(subroutines.doLegend).toHaveBeenCalledTimes(1); + expect(calcdata).toBe(gd.calcdata); + }) + .then(done, done.fail); }); - it('call array manager when adding update menu', function(done) { + it('call array manager when adding update menu', function (done) { spyOn(manageArrays, 'applyContainerArrayChanges').and.callThrough(); var layoutUpdate = { - updatemenus: [{ - buttons: [{ - method: 'relayout', - args: ['title', 'Hello World'] - }] - }] + updatemenus: [ + { + buttons: [ + { + method: 'relayout', + args: ['title', 'Hello World'] + } + ] + } + ] }; - Plotly.update(gd, {}, layoutUpdate).then(function() { - expect(manageArrays.applyContainerArrayChanges).toHaveBeenCalledTimes(1); - expect(subroutines.layoutReplot).toHaveBeenCalledTimes(0); - expect(calcdata).toBe(gd.calcdata); - }) - .then(done, done.fail); + Plotly.update(gd, {}, layoutUpdate) + .then(function () { + expect(manageArrays.applyContainerArrayChanges).toHaveBeenCalledTimes(1); + expect(subroutines.layoutReplot).toHaveBeenCalledTimes(0); + expect(calcdata).toBe(gd.calcdata); + }) + .then(done, done.fail); }); - it('call doModeBar when updating \'dragmode\'', function(done) { - Plotly.update(gd, {}, { dragmode: 'pan' }).then(function() { - expect(subroutines.doModeBar).toHaveBeenCalledTimes(1); - expect(calcdata).toBe(gd.calcdata); - }) - .then(done, done.fail); + it("call doModeBar when updating 'dragmode'", function (done) { + Plotly.update(gd, {}, { dragmode: 'pan' }) + .then(function () { + expect(subroutines.doModeBar).toHaveBeenCalledTimes(1); + expect(calcdata).toBe(gd.calcdata); + }) + .then(done, done.fail); }); - it('ignores invalid trace indices', function() { + it('ignores invalid trace indices', function () { // Call update on an invalid trace indice - Plotly.update(gd, {type: 'scatter', 'marker.color': 'red'}, {}, [1]); + Plotly.update(gd, { type: 'scatter', 'marker.color': 'red' }, {}, [1]); }); }); }); -describe('plot_api helpers', function() { - describe('hasParent', function() { +describe('plot_api helpers', function () { + describe('hasParent', function () { var attr = 'annotations[2].xref'; var attr2 = 'marker.line.width'; - it('does not match the attribute itself or other related non-parent attributes', function() { + it('does not match the attribute itself or other related non-parent attributes', function () { var aobj = { // '' wouldn't be valid as an attribute in our framework, but tested // just in case this would count as a parent. @@ -2729,7 +3075,7 @@ describe('plot_api helpers', function() { expect(helpers.hasParent(aobj2, attr2)).toBe(false); }); - it('is false when called on a top-level attribute', function() { + it('is false when called on a top-level attribute', function () { var aobj = { '': true, width: 100 @@ -2738,49 +3084,90 @@ describe('plot_api helpers', function() { expect(helpers.hasParent(aobj, 'width')).toBe(false); }); - it('matches any kind of parent', function() { - expect(helpers.hasParent({annotations: []}, attr)).toBe(true); - expect(helpers.hasParent({'annotations[2]': {}}, attr)).toBe(true); + it('matches any kind of parent', function () { + expect(helpers.hasParent({ annotations: [] }, attr)).toBe(true); + expect(helpers.hasParent({ 'annotations[2]': {} }, attr)).toBe(true); - expect(helpers.hasParent({marker: {}}, attr2)).toBe(true); + expect(helpers.hasParent({ marker: {} }, attr2)).toBe(true); // this one wouldn't actually make sense: marker.line needs to be an object... // but hasParent doesn't look at the values in aobj, just its keys. - expect(helpers.hasParent({'marker.line': 1}, attr2)).toBe(true); + expect(helpers.hasParent({ 'marker.line': 1 }, attr2)).toBe(true); + }); + }); + + describe('hasCollectionChanged', () => { + it('Returns true if object collection has changed', () => { + expect( + helpers.hasCollectionChanged( + { captain: 'Leela', deliveryBoy: 'Fry' }, + { captain: 'Leela', deliveryBoy: 'Bender' } + ) + ).toBe(true); + }); + + it("Returns false if object collection hasn't changed", () => { + expect( + helpers.hasCollectionChanged( + { captain: 'Leela', deliveryBoy: 'Fry' }, + { captain: 'Leela', deliveryBoy: 'Fry' } + ) + ).toBe(false); + }); + + it('Returns true if array collection has changed', () => { + expect(helpers.hasCollectionChanged(['Zoidberg', 'Hermes'], ['Zoidberg', 'Leela'])).toBe(true); + }); + + it("Returns false if array collection hasn't changed", () => { + expect(helpers.hasCollectionChanged(['Zoidberg', 'Hermes'], ['Zoidberg', 'Hermes'])).toBe(false); + }); + + it('Handles nested objects', () => { + expect( + helpers.hasCollectionChanged( + { level1: { captain: 'Leela', deliveryBoy: 'Fry' } }, + { level1: { captain: 'Leela', deliveryBoy: 'Bender' } } + ) + ).toBe(true); + }); + + it('Handles nested arrays', () => { + expect(helpers.hasCollectionChanged([['Zoidberg', 'Hermes']], [['Zoidberg', 'Leela']])).toBe(true); }); }); }); -describe('plot_api edit_types', function() { - it('initializes flags with all false', function() { - ['traceFlags', 'layoutFlags'].forEach(function(container) { +describe('plot_api edit_types', function () { + it('initializes flags with all false', function () { + ['traceFlags', 'layoutFlags'].forEach(function (container) { var initFlags = editTypes[container](); - Object.keys(initFlags).forEach(function(key) { + Object.keys(initFlags).forEach(function (key) { expect(initFlags[key]).toBe(false, container + '.' + key); }); }); }); - it('makes no changes if editType is not included', function() { - var flags = {calc: false, style: true}; + it('makes no changes if editType is not included', function () { + var flags = { calc: false, style: true }; editTypes.update(flags, { valType: 'boolean', dflt: true }); - expect(flags).toEqual({calc: false, style: true}); + expect(flags).toEqual({ calc: false, style: true }); editTypes.update(flags, { - family: {valType: 'string', dflt: 'Comic sans'}, - size: {valType: 'number', dflt: 96}, - color: {valType: 'color', dflt: 'red'} + family: { valType: 'string', dflt: 'Comic sans' }, + size: { valType: 'number', dflt: 96 }, + color: { valType: 'color', dflt: 'red' } }); - expect(flags).toEqual({calc: false, style: true}); + expect(flags).toEqual({ calc: false, style: true }); }); - it('gets updates from the outer object and ignores nested items', function() { - var flags = {calc: false, legend: true}; + it('gets updates from the outer object and ignores nested items', function () { + var flags = { calc: false, legend: true }; editTypes.update(flags, { editType: 'calc+style', @@ -2788,6 +3175,6 @@ describe('plot_api edit_types', function() { dflt: 1 }); - expect(flags).toEqual({calc: true, legend: true, style: true}); + expect(flags).toEqual({ calc: true, legend: true, style: true }); }); });