'use strict'; var d3 = require('@plotly/d3'); var timeFormatLocale = require('d3-time-format').timeFormatLocale; var formatLocale = require('d3-format').formatLocale; var isNumeric = require('fast-isnumeric'); var b64encode = require('base64-arraybuffer'); var Registry = require('../registry'); var PlotSchema = require('../plot_api/plot_schema'); var Template = require('../plot_api/plot_template'); var Lib = require('../lib'); var Color = require('../components/color'); var BADNUM = require('../constants/numerical').BADNUM; var axisIDs = require('./cartesian/axis_ids'); var clearOutline = require('../components/shapes/handle_outline').clearOutline; var scatterAttrs = require('../traces/scatter/layout_attributes'); var animationAttrs = require('./animation_attributes'); var frameAttrs = require('./frame_attributes'); var getModuleCalcData = require('../plots/get_data').getModuleCalcData; var relinkPrivateKeys = Lib.relinkPrivateKeys; var _ = Lib._; var plots = module.exports = {}; // Expose registry methods on Plots for backward-compatibility Lib.extendFlat(plots, Registry); plots.attributes = require('./attributes'); plots.attributes.type.values = plots.allTypes; plots.fontAttrs = require('./font_attributes'); plots.layoutAttributes = require('./layout_attributes'); var transformsRegistry = plots.transformsRegistry; var commandModule = require('./command'); plots.executeAPICommand = commandModule.executeAPICommand; plots.computeAPICommandBindings = commandModule.computeAPICommandBindings; plots.manageCommandObserver = commandModule.manageCommandObserver; plots.hasSimpleAPICommandBindings = commandModule.hasSimpleAPICommandBindings; // in some cases the browser doesn't seem to know how big // the text is at first, so it needs to draw it, // then wait a little, then draw it again plots.redrawText = function(gd) { gd = Lib.getGraphDiv(gd); return new Promise(function(resolve) { setTimeout(function() { if(!gd._fullLayout) return; Registry.getComponentMethod('annotations', 'draw')(gd); Registry.getComponentMethod('legend', 'draw')(gd); Registry.getComponentMethod('colorbar', 'draw')(gd); resolve(plots.previousPromises(gd)); }, 300); }); }; // resize plot about the container size plots.resize = function(gd) { gd = Lib.getGraphDiv(gd); var resolveLastResize; var p = new Promise(function(resolve, reject) { if(!gd || Lib.isHidden(gd)) { reject(new Error('Resize must be passed a displayed plot div element.')); } if(gd._redrawTimer) clearTimeout(gd._redrawTimer); if(gd._resolveResize) resolveLastResize = gd._resolveResize; gd._resolveResize = resolve; gd._redrawTimer = setTimeout(function() { // return if there is nothing to resize or is hidden if(!gd.layout || (gd.layout.width && gd.layout.height) || Lib.isHidden(gd)) { resolve(gd); return; } delete gd.layout.width; delete gd.layout.height; // autosizing doesn't count as a change that needs saving var oldchanged = gd.changed; // nor should it be included in the undo queue gd.autoplay = true; Registry.call('relayout', gd, {autosize: true}).then(function() { gd.changed = oldchanged; // Only resolve if a new call hasn't been made! if(gd._resolveResize === resolve) { delete gd._resolveResize; resolve(gd); } }); }, 100); }); if(resolveLastResize) resolveLastResize(p); return p; }; // for use in Lib.syncOrAsync, check if there are any // pending promises in this plot and wait for them plots.previousPromises = function(gd) { if((gd._promises || []).length) { return Promise.all(gd._promises) .then(function() { gd._promises = []; }); } }; /** * Adds the 'Edit chart' link. * Note that now _doPlot calls this so it can regenerate whenever it replots * * Add source links to your graph inside the 'showSources' config argument. */ plots.addLinks = function(gd) { // Do not do anything if showLink and showSources are not set to true in config if(!gd._context.showLink && !gd._context.showSources) return; var fullLayout = gd._fullLayout; var linkContainer = Lib.ensureSingle(fullLayout._paper, 'text', 'js-plot-link-container', function(s) { s.style({ 'font-family': '"Open Sans", Arial, sans-serif', 'font-size': '12px', fill: Color.defaultLine, 'pointer-events': 'all' }) .each(function() { var links = d3.select(this); links.append('tspan').classed('js-link-to-tool', true); links.append('tspan').classed('js-link-spacer', true); links.append('tspan').classed('js-sourcelinks', true); }); }); // The text node inside svg var text = linkContainer.node(); var attrs = {y: fullLayout._paper.attr('height') - 9}; // If text's width is bigger than the layout // Check that text is a child node or document.body // because otherwise IE/Edge might throw an exception // when calling getComputedTextLength(). // Apparently offsetParent is null for invisibles. if(document.body.contains(text) && text.getComputedTextLength() >= (fullLayout.width - 20)) { // Align the text at the left attrs['text-anchor'] = 'start'; attrs.x = 5; } else { // Align the text at the right attrs['text-anchor'] = 'end'; attrs.x = fullLayout._paper.attr('width') - 7; } linkContainer.attr(attrs); var toolspan = linkContainer.select('.js-link-to-tool'); var spacespan = linkContainer.select('.js-link-spacer'); var sourcespan = linkContainer.select('.js-sourcelinks'); if(gd._context.showSources) gd._context.showSources(gd); // 'view in plotly' link for embedded plots if(gd._context.showLink) positionPlayWithData(gd, toolspan); // separator if we have both sources and tool link spacespan.text((toolspan.text() && sourcespan.text()) ? ' - ' : ''); }; // note that now this function is only adding the brand in // iframes and 3rd-party apps function positionPlayWithData(gd, container) { container.text(''); var link = container.append('a') .attr({ 'xlink:xlink:href': '#', class: 'link--impt link--embedview', 'font-weight': 'bold' }) .text(gd._context.linkText + ' ' + String.fromCharCode(187)); if(gd._context.sendData) { link.on('click', function() { plots.sendDataToCloud(gd); }); } else { var path = window.location.pathname.split('/'); var query = window.location.search; link.attr({ 'xlink:xlink:show': 'new', 'xlink:xlink:href': '/' + path[2].split('.')[0] + '/' + path[1] + query }); } } plots.sendDataToCloud = function(gd) { var baseUrl = (window.PLOTLYENV || {}).BASE_URL || gd._context.plotlyServerURL; if(!baseUrl) return; gd.emit('plotly_beforeexport'); var hiddenformDiv = d3.select(gd) .append('div') .attr('id', 'hiddenform') .style('display', 'none'); var hiddenform = hiddenformDiv .append('form') .attr({ action: baseUrl + '/external', method: 'post', target: '_blank' }); var hiddenformInput = hiddenform .append('input') .attr({ type: 'text', name: 'data' }); hiddenformInput.node().value = plots.graphJson(gd, false, 'keepdata'); hiddenform.node().submit(); hiddenformDiv.remove(); gd.emit('plotly_afterexport'); return false; }; var d3FormatKeys = [ 'days', 'shortDays', 'months', 'shortMonths', 'periods', 'dateTime', 'date', 'time', 'decimal', 'thousands', 'grouping', 'currency' ]; var extraFormatKeys = [ 'year', 'month', 'dayMonth', 'dayMonthYear' ]; /* * Fill in default values * @param {DOM element} gd * @param {object} opts * @param {boolean} opts.skipUpdateCalc: normally if the existing gd.calcdata looks * compatible with the new gd._fullData we finish by linking the new _fullData traces * to the old gd.calcdata, so it's correctly set if we're not going to recalc. But also, * if there are calcTransforms on the trace, we first remap data arrays from the old full * trace into the new one. Use skipUpdateCalc to defer this (needed by Plotly.react) * * gd.data, gd.layout: * are precisely what the user specified (except as modified by cleanData/cleanLayout), * these fields shouldn't be modified (except for filling in some auto values) * nor used directly after the supply defaults step. * * gd._fullData, gd._fullLayout: * are complete descriptions of how to draw the plot, * use these fields in all required computations. * * gd._fullLayout._modules * is a list of all the trace modules required to draw the plot. * * gd._fullLayout._visibleModules * subset of _modules, a list of modules corresponding to visible:true traces. * * gd._fullLayout._basePlotModules * is a list of all the plot modules required to draw the plot. * * gd._fullLayout._transformModules * is a list of all the transform modules invoked. * */ plots.supplyDefaults = function(gd, opts) { var skipUpdateCalc = opts && opts.skipUpdateCalc; var oldFullLayout = gd._fullLayout || {}; if(oldFullLayout._skipDefaults) { delete oldFullLayout._skipDefaults; return; } var newFullLayout = gd._fullLayout = {}; var newLayout = gd.layout || {}; var oldFullData = gd._fullData || []; var newFullData = gd._fullData = []; var newData = gd.data || []; var oldCalcdata = gd.calcdata || []; var context = gd._context || {}; var i; // Create all the storage space for frames, but only if doesn't already exist if(!gd._transitionData) plots.createTransitionData(gd); // So we only need to do this once (and since we have gd here) // get the translated placeholder titles. // These ones get used as default values so need to be known at supplyDefaults // others keep their blank defaults but render the placeholder as desired later // TODO: make these work the same way, only inserting the placeholder text at draw time? // The challenge is that this has slightly different behavior right now in editable mode: // using the placeholder as default makes this text permanently (but lightly) visible, // but explicit '' for these titles gives you a placeholder that's hidden until you mouse // over it - so you're not distracted by it if you really don't want a title, but if you do // and you're new to plotly you may not be able to find it. // When editable=false the two behave the same, no title is drawn. newFullLayout._dfltTitle = { plot: _(gd, 'Click to enter Plot title'), subtitle: _(gd, 'Click to enter Plot subtitle'), x: _(gd, 'Click to enter X axis title'), y: _(gd, 'Click to enter Y axis title'), colorbar: _(gd, 'Click to enter Colorscale title'), annotation: _(gd, 'new text') }; newFullLayout._traceWord = _(gd, 'trace'); var formatObj = getFormatObj(gd, d3FormatKeys); // stash the token from context so mapbox subplots can use it as default newFullLayout._mapboxAccessToken = context.mapboxAccessToken; // first fill in what we can of layout without looking at data // because fullData needs a few things from layout if(oldFullLayout._initialAutoSizeIsDone) { // coerce the updated layout while preserving width and height var oldWidth = oldFullLayout.width; var oldHeight = oldFullLayout.height; plots.supplyLayoutGlobalDefaults(newLayout, newFullLayout, formatObj); if(!newLayout.width) newFullLayout.width = oldWidth; if(!newLayout.height) newFullLayout.height = oldHeight; plots.sanitizeMargins(newFullLayout); } else { // coerce the updated layout and autosize if needed plots.supplyLayoutGlobalDefaults(newLayout, newFullLayout, formatObj); var missingWidthOrHeight = (!newLayout.width || !newLayout.height); var autosize = newFullLayout.autosize; var autosizable = context.autosizable; var initialAutoSize = missingWidthOrHeight && (autosize || autosizable); if(initialAutoSize) plots.plotAutoSize(gd, newLayout, newFullLayout); else if(missingWidthOrHeight) plots.sanitizeMargins(newFullLayout); // for backwards-compatibility with Plotly v1.x.x if(!autosize && missingWidthOrHeight) { newLayout.width = newFullLayout.width; newLayout.height = newFullLayout.height; } } newFullLayout._d3locale = getFormatter(formatObj, newFullLayout.separators); newFullLayout._extraFormat = getFormatObj(gd, extraFormatKeys); newFullLayout._initialAutoSizeIsDone = true; // keep track of how many traces are inputted newFullLayout._dataLength = newData.length; // clear the lists of trace and baseplot modules, and subplots newFullLayout._modules = []; newFullLayout._visibleModules = []; newFullLayout._basePlotModules = []; var subplots = newFullLayout._subplots = emptySubplotLists(); // initialize axis and subplot hash objects for splom-generated grids var splomAxes = newFullLayout._splomAxes = {x: {}, y: {}}; var splomSubplots = newFullLayout._splomSubplots = {}; // initialize splom grid defaults newFullLayout._splomGridDflt = {}; // for stacked area traces to share config across traces newFullLayout._scatterStackOpts = {}; // for the first scatter trace on each subplot (so it knows tonext->tozero) newFullLayout._firstScatter = {}; // for grouped bar/box/violin trace to share config across traces newFullLayout._alignmentOpts = {}; // track color axes referenced in the data newFullLayout._colorAxes = {}; // for traces to request a default rangeslider on their x axes // eg set `_requestRangeslider.x2 = true` for xaxis2 newFullLayout._requestRangeslider = {}; // pull uids from old data to use as new defaults newFullLayout._traceUids = getTraceUids(oldFullData, newData); // then do the data newFullLayout._globalTransforms = (gd._context || {}).globalTransforms; plots.supplyDataDefaults(newData, newFullData, newLayout, newFullLayout); // redo grid size defaults with info about splom x/y axes, // and fill in generated cartesian axes and subplots var splomXa = Object.keys(splomAxes.x); var splomYa = Object.keys(splomAxes.y); if(splomXa.length > 1 && splomYa.length > 1) { Registry.getComponentMethod('grid', 'sizeDefaults')(newLayout, newFullLayout); for(i = 0; i < splomXa.length; i++) { Lib.pushUnique(subplots.xaxis, splomXa[i]); } for(i = 0; i < splomYa.length; i++) { Lib.pushUnique(subplots.yaxis, splomYa[i]); } for(var k in splomSubplots) { Lib.pushUnique(subplots.cartesian, k); } } // attach helper method to check whether a plot type is present on graph newFullLayout._has = plots._hasPlotType.bind(newFullLayout); if(oldFullData.length === newFullData.length) { for(i = 0; i < newFullData.length; i++) { relinkPrivateKeys(newFullData[i], oldFullData[i]); } } // finally, fill in the pieces of layout that may need to look at data plots.supplyLayoutModuleDefaults(newLayout, newFullLayout, newFullData, gd._transitionData); // Special cases that introduce interactions between traces. // This is after relinkPrivateKeys so we can use those in crossTraceDefaults // and after layout module defaults, so we can use eg barmode var _modules = newFullLayout._visibleModules; var crossTraceDefaultsFuncs = []; for(i = 0; i < _modules.length; i++) { var funci = _modules[i].crossTraceDefaults; // some trace types share crossTraceDefaults (ie histogram2d, histogram2dcontour) if(funci) Lib.pushUnique(crossTraceDefaultsFuncs, funci); } for(i = 0; i < crossTraceDefaultsFuncs.length; i++) { crossTraceDefaultsFuncs[i](newFullData, newFullLayout); } // turn on flag to optimize large splom-only graphs // mostly by omitting SVG layers during Cartesian.drawFramework newFullLayout._hasOnlyLargeSploms = ( newFullLayout._basePlotModules.length === 1 && newFullLayout._basePlotModules[0].name === 'splom' && splomXa.length > 15 && splomYa.length > 15 && newFullLayout.shapes.length === 0 && newFullLayout.images.length === 0 ); // relink / initialize subplot axis objects plots.linkSubplots(newFullData, newFullLayout, oldFullData, oldFullLayout); // clean subplots and other artifacts from previous plot calls plots.cleanPlot(newFullData, newFullLayout, oldFullData, oldFullLayout); var hadGL2D = !!(oldFullLayout._has && oldFullLayout._has('gl2d')); var hasGL2D = !!(newFullLayout._has && newFullLayout._has('gl2d')); var hadCartesian = !!(oldFullLayout._has && oldFullLayout._has('cartesian')); var hasCartesian = !!(newFullLayout._has && newFullLayout._has('cartesian')); var hadBgLayer = hadCartesian || hadGL2D; var hasBgLayer = hasCartesian || hasGL2D; if(hadBgLayer && !hasBgLayer) { // remove bgLayer oldFullLayout._bgLayer.remove(); } else if(hasBgLayer && !hadBgLayer) { // create bgLayer newFullLayout._shouldCreateBgLayer = true; } // clear selection outline until we implement persistent selection, // don't clear them though when drag handlers (e.g. listening to // `plotly_selecting`) update the graph. // we should try to come up with a better solution when implementing // https://github.com/plotly/plotly.js/issues/1851 if(oldFullLayout._zoomlayer && !gd._dragging) { clearOutline({ // mock old gd _fullLayout: oldFullLayout }); } // fill in meta helpers fillMetaTextHelpers(newFullData, newFullLayout); // relink functions and _ attributes to promote consistency between plots relinkPrivateKeys(newFullLayout, oldFullLayout); // colorscale crossTraceDefaults needs newFullLayout with relinked keys Registry.getComponentMethod('colorscale', 'crossTraceDefaults')(newFullData, newFullLayout); // For persisting GUI-driven changes in layout // _preGUI and _tracePreGUI were already copied over in relinkPrivateKeys if(!newFullLayout._preGUI) newFullLayout._preGUI = {}; // track trace GUI changes by uid rather than by trace index if(!newFullLayout._tracePreGUI) newFullLayout._tracePreGUI = {}; var tracePreGUI = newFullLayout._tracePreGUI; var uids = {}; var uid; for(uid in tracePreGUI) uids[uid] = 'old'; for(i = 0; i < newFullData.length; i++) { uid = newFullData[i]._fullInput.uid; if(!uids[uid]) tracePreGUI[uid] = {}; uids[uid] = 'new'; } for(uid in uids) { if(uids[uid] === 'old') delete tracePreGUI[uid]; } // set up containers for margin calculations initMargins(newFullLayout); // collect and do some initial calculations for rangesliders Registry.getComponentMethod('rangeslider', 'makeData')(newFullLayout); // update object references in calcdata if(!skipUpdateCalc && oldCalcdata.length === newFullData.length) { plots.supplyDefaultsUpdateCalc(oldCalcdata, newFullData); } }; plots.supplyDefaultsUpdateCalc = function(oldCalcdata, newFullData) { for(var i = 0; i < newFullData.length; i++) { var newTrace = newFullData[i]; var cd0 = (oldCalcdata[i] || [])[0]; if(cd0 && cd0.trace) { var oldTrace = cd0.trace; if(oldTrace._hasCalcTransform) { var arrayAttrs = oldTrace._arrayAttrs; var j, astr, oldArrayVal; for(j = 0; j < arrayAttrs.length; j++) { astr = arrayAttrs[j]; oldArrayVal = Lib.nestedProperty(oldTrace, astr).get().slice(); Lib.nestedProperty(newTrace, astr).set(oldArrayVal); } } cd0.trace = newTrace; } } }; /** * Create a list of uid strings satisfying (in this order of importance): * 1. all unique, all strings * 2. matches input uids if provided * 3. matches previous data uids */ function getTraceUids(oldFullData, newData) { var len = newData.length; var oldFullInput = []; var i, prevFullInput; for(i = 0; i < oldFullData.length; i++) { var thisFullInput = oldFullData[i]._fullInput; if(thisFullInput !== prevFullInput) oldFullInput.push(thisFullInput); prevFullInput = thisFullInput; } var oldLen = oldFullInput.length; var out = new Array(len); var seenUids = {}; function setUid(uid, i) { out[i] = uid; seenUids[uid] = 1; } function tryUid(uid, i) { if(uid && typeof uid === 'string' && !seenUids[uid]) { setUid(uid, i); return true; } } for(i = 0; i < len; i++) { var newUid = newData[i].uid; if(typeof newUid === 'number') newUid = String(newUid); if(tryUid(newUid, i)) continue; if(i < oldLen && tryUid(oldFullInput[i].uid, i)) continue; setUid(Lib.randstr(seenUids), i); } return out; } /** * Make a container for collecting subplots we need to display. * * Finds all subplot types we need to enumerate once and caches it, * but makes a new output object each time. * Single-trace subplots (which have no `id`) such as pie, table, etc * do not need to be collected because we just draw all visible traces. */ function emptySubplotLists() { var collectableSubplotTypes = Registry.collectableSubplotTypes; var out = {}; var i, j; if(!collectableSubplotTypes) { collectableSubplotTypes = []; var subplotsRegistry = Registry.subplotsRegistry; for(var subplotType in subplotsRegistry) { var subplotModule = subplotsRegistry[subplotType]; var subplotAttr = subplotModule.attr; if(subplotAttr) { collectableSubplotTypes.push(subplotType); // special case, currently just for cartesian: // we need to enumerate axes, not just subplots if(Array.isArray(subplotAttr)) { for(j = 0; j < subplotAttr.length; j++) { Lib.pushUnique(collectableSubplotTypes, subplotAttr[j]); } } } } } for(i = 0; i < collectableSubplotTypes.length; i++) { out[collectableSubplotTypes[i]] = []; } return out; } /** * getFormatObj: use _context to get the format object from locale. * Used to get d3.locale argument object and extraFormat argument object * * Regarding d3.locale argument : * decimal and thousands can be overridden later by layout.separators * grouping and currency are not presently used by our automatic number * formatting system but can be used by custom formats. * * @returns {object} d3.locale format object */ function getFormatObj(gd, formatKeys) { var locale = gd._context.locale; if(!locale) locale = 'en-US'; var formatDone = false; var formatObj = {}; function includeFormat(newFormat) { var formatFinished = true; for(var i = 0; i < formatKeys.length; i++) { var formatKey = formatKeys[i]; if(!formatObj[formatKey]) { if(newFormat[formatKey]) { formatObj[formatKey] = newFormat[formatKey]; } else formatFinished = false; } } if(formatFinished) formatDone = true; } // same as localize, look for format parts in each format spec in the chain for(var i = 0; i < 2; i++) { var locales = gd._context.locales; for(var j = 0; j < 2; j++) { var formatj = (locales[locale] || {}).format; if(formatj) { includeFormat(formatj); if(formatDone) break; } locales = Registry.localeRegistry; } var baseLocale = locale.split('-')[0]; if(formatDone || baseLocale === locale) break; locale = baseLocale; } // lastly pick out defaults from english (non-US, as DMY is so much more common) if(!formatDone) includeFormat(Registry.localeRegistry.en.format); return formatObj; } /** * getFormatter: combine the final separators with the locale formatting object * we pulled earlier to generate number and time formatters * TODO: remove separators in v3, only use locale, so we don't need this step? * * @param {object} formatObj: d3.locale format object * @param {string} separators: length-2 string to override decimal and thousands * separators in number formatting * * @returns {object} {numberFormat, timeFormat} d3 formatter factory functions * for numbers and time */ function getFormatter(formatObj, separators) { formatObj.decimal = separators.charAt(0); formatObj.thousands = separators.charAt(1); return { numberFormat: function(formatStr) { try { formatStr = formatLocale(formatObj).format( Lib.adjustFormat(formatStr) ); } catch(e) { Lib.warnBadFormat(formatStr); return Lib.noFormat; } return formatStr; }, timeFormat: timeFormatLocale(formatObj).utcFormat }; } function fillMetaTextHelpers(newFullData, newFullLayout) { var _meta; var meta4data = []; if(newFullLayout.meta) { _meta = newFullLayout._meta = { meta: newFullLayout.meta, layout: {meta: newFullLayout.meta} }; } for(var i = 0; i < newFullData.length; i++) { var trace = newFullData[i]; if(trace.meta) { meta4data[trace.index] = trace._meta = {meta: trace.meta}; } else if(newFullLayout.meta) { trace._meta = {meta: newFullLayout.meta}; } if(newFullLayout.meta) { trace._meta.layout = {meta: newFullLayout.meta}; } } if(meta4data.length) { if(!_meta) { _meta = newFullLayout._meta = {}; } _meta.data = meta4data; } } // Create storage for all of the data related to frames and transitions: plots.createTransitionData = function(gd) { // Set up the default keyframe if it doesn't exist: if(!gd._transitionData) { gd._transitionData = {}; } if(!gd._transitionData._frames) { gd._transitionData._frames = []; } if(!gd._transitionData._frameHash) { gd._transitionData._frameHash = {}; } if(!gd._transitionData._counter) { gd._transitionData._counter = 0; } if(!gd._transitionData._interruptCallbacks) { gd._transitionData._interruptCallbacks = []; } }; // helper function to be bound to fullLayout to check // whether a certain plot type is present on plot // or trace has a category plots._hasPlotType = function(category) { var i; // check base plot modules var basePlotModules = this._basePlotModules || []; for(i = 0; i < basePlotModules.length; i++) { if(basePlotModules[i].name === category) return true; } // check trace modules (including non-visible:true) var modules = this._modules || []; for(i = 0; i < modules.length; i++) { var name = modules[i].name; if(name === category) return true; // N.B. this is modules[i] along with 'categories' as a hash object var _module = Registry.modules[name]; if(_module && _module.categories[category]) return true; } return false; }; plots.cleanPlot = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { var i, j; var basePlotModules = oldFullLayout._basePlotModules || []; for(i = 0; i < basePlotModules.length; i++) { var _module = basePlotModules[i]; if(_module.clean) { _module.clean(newFullData, newFullLayout, oldFullData, oldFullLayout); } } var hadGl = oldFullLayout._has && oldFullLayout._has('gl'); var hasGl = newFullLayout._has && newFullLayout._has('gl'); if(hadGl && !hasGl) { if(oldFullLayout._glcontainer !== undefined) { oldFullLayout._glcontainer.selectAll('.gl-canvas').remove(); oldFullLayout._glcontainer.selectAll('.no-webgl').remove(); oldFullLayout._glcanvas = null; } } var hasInfoLayer = !!oldFullLayout._infolayer; oldLoop: for(i = 0; i < oldFullData.length; i++) { var oldTrace = oldFullData[i]; var oldUid = oldTrace.uid; for(j = 0; j < newFullData.length; j++) { var newTrace = newFullData[j]; if(oldUid === newTrace.uid) continue oldLoop; } // clean old colorbars if(hasInfoLayer) { oldFullLayout._infolayer.select('.cb' + oldUid).remove(); } } }; plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { var i, j; var oldSubplots = oldFullLayout._plots || {}; var newSubplots = newFullLayout._plots = {}; var newSubplotList = newFullLayout._subplots; var mockGd = { _fullData: newFullData, _fullLayout: newFullLayout }; var ids = newSubplotList.cartesian.concat(newSubplotList.gl2d || []); for(i = 0; i < ids.length; i++) { var id = ids[i]; var oldSubplot = oldSubplots[id]; var xaxis = axisIDs.getFromId(mockGd, id, 'x'); var yaxis = axisIDs.getFromId(mockGd, id, 'y'); var plotinfo; // link or create subplot object if(oldSubplot) { plotinfo = newSubplots[id] = oldSubplot; } else { plotinfo = newSubplots[id] = {}; plotinfo.id = id; } // add these axis ids to each others' subplot lists xaxis._counterAxes.push(yaxis._id); yaxis._counterAxes.push(xaxis._id); xaxis._subplotsWith.push(id); yaxis._subplotsWith.push(id); // update x and y axis layout object refs plotinfo.xaxis = xaxis; plotinfo.yaxis = yaxis; // By default, we clip at the subplot level, // but if one trace on a given subplot has *cliponaxis* set to false, // we need to clip at the trace module layer level; // find this out here, once of for all. plotinfo._hasClipOnAxisFalse = false; for(j = 0; j < newFullData.length; j++) { var trace = newFullData[j]; if( trace.xaxis === plotinfo.xaxis._id && trace.yaxis === plotinfo.yaxis._id && trace.cliponaxis === false ) { plotinfo._hasClipOnAxisFalse = true; break; } } } // while we're at it, link overlaying axes to their main axes and // anchored axes to the axes they're anchored to var axList = axisIDs.list(mockGd, null, true); var ax; for(i = 0; i < axList.length; i++) { ax = axList[i]; var mainAx = null; if(ax.overlaying) { mainAx = axisIDs.getFromId(mockGd, ax.overlaying); // you cannot overlay an axis that's already overlaying another if(mainAx && mainAx.overlaying) { ax.overlaying = false; mainAx = null; } } ax._mainAxis = mainAx || ax; /* * For now force overlays to overlay completely... so they * can drag together correctly and share backgrounds. * Later perhaps we make separate axis domain and * tick/line domain or something, so they can still share * the (possibly larger) dragger and background but don't * have to both be drawn over that whole domain */ if(mainAx) ax.domain = mainAx.domain.slice(); ax._anchorAxis = ax.anchor === 'free' ? null : axisIDs.getFromId(mockGd, ax.anchor); } // finally, we can find the main subplot for each axis // (on which the ticks & labels are drawn) for(i = 0; i < axList.length; i++) { ax = axList[i]; ax._counterAxes.sort(axisIDs.idSort); ax._subplotsWith.sort(Lib.subplotSort); ax._mainSubplot = findMainSubplot(ax, newFullLayout); // find "full" domain span of counter axes, // this loop can be costly, so only compute it when required if(ax._counterAxes.length && ( (ax.spikemode && ax.spikemode.indexOf('across') !== -1) || (ax.automargin && ax.mirror && ax.anchor !== 'free') || Registry.getComponentMethod('rangeslider', 'isVisible')(ax) )) { var min = 1; var max = 0; for(j = 0; j < ax._counterAxes.length; j++) { var ax2 = axisIDs.getFromId(mockGd, ax._counterAxes[j]); min = Math.min(min, ax2.domain[0]); max = Math.max(max, ax2.domain[1]); } if(min < max) { ax._counterDomainMin = min; ax._counterDomainMax = max; } } } }; function findMainSubplot(ax, fullLayout) { var mockGd = {_fullLayout: fullLayout}; var isX = ax._id.charAt(0) === 'x'; var anchorAx = ax._mainAxis._anchorAxis; var mainSubplotID = ''; var nextBestMainSubplotID = ''; var anchorID = ''; // First try the main ID with the anchor if(anchorAx) { anchorID = anchorAx._mainAxis._id; mainSubplotID = isX ? (ax._id + anchorID) : (anchorID + ax._id); } // Then look for a subplot with the counteraxis overlaying the anchor // If that fails just use the first subplot including this axis if(!mainSubplotID || !fullLayout._plots[mainSubplotID]) { mainSubplotID = ''; var counterIDs = ax._counterAxes; for(var j = 0; j < counterIDs.length; j++) { var counterPart = counterIDs[j]; var id = isX ? (ax._id + counterPart) : (counterPart + ax._id); if(!nextBestMainSubplotID) nextBestMainSubplotID = id; var counterAx = axisIDs.getFromId(mockGd, counterPart); if(anchorID && counterAx.overlaying === anchorID) { mainSubplotID = id; break; } } } return mainSubplotID || nextBestMainSubplotID; } // This function clears any trace attributes with valType: color and // no set dflt filed in the plot schema. This is needed because groupby (which // is the only transform for which this currently applies) supplies parent // trace defaults, then expanded trace defaults. The result is that `null` // colors are default-supplied and inherited as a color instead of a null. // The result is that expanded trace default colors have no effect, with // the final result that groups are indistinguishable. This function clears // those colors so that individual groupby groups get unique colors. plots.clearExpandedTraceDefaultColors = function(trace) { var colorAttrs, path, i; // This uses weird closure state in order to satisfy the linter rule // that we can't create functions in a loop. function locateColorAttrs(attr, attrName, attrs, level) { path[level] = attrName; path.length = level + 1; if(attr.valType === 'color' && attr.dflt === undefined) { colorAttrs.push(path.join('.')); } } path = []; // Get the cached colorAttrs: colorAttrs = trace._module._colorAttrs; // Or else compute and cache the colorAttrs on the module: if(!colorAttrs) { trace._module._colorAttrs = colorAttrs = []; PlotSchema.crawl( trace._module.attributes, locateColorAttrs ); } for(i = 0; i < colorAttrs.length; i++) { var origprop = Lib.nestedProperty(trace, '_input.' + colorAttrs[i]); if(!origprop.get()) { Lib.nestedProperty(trace, colorAttrs[i]).set(null); } } }; plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { var modules = fullLayout._modules; var visibleModules = fullLayout._visibleModules; var basePlotModules = fullLayout._basePlotModules; var cnt = 0; var colorCnt = 0; var i, fullTrace, trace; fullLayout._transformModules = []; function pushModule(fullTrace) { dataOut.push(fullTrace); var _module = fullTrace._module; if(!_module) return; Lib.pushUnique(modules, _module); if(fullTrace.visible === true) Lib.pushUnique(visibleModules, _module); Lib.pushUnique(basePlotModules, fullTrace._module.basePlotModule); cnt++; // TODO: do we really want color not to increment for explicitly invisible traces? // This logic is weird, but matches previous behavior: traces that you explicitly // set to visible:false do not increment the color, but traces WE determine to be // empty or invalid (and thus set to visible:false) DO increment color. // I kind of think we should just let all traces increment color, visible or not. // see mock: axes-autotype-empty vs. a test of restyling visible: false that // I can't find right now... if(fullTrace._input.visible !== false) colorCnt++; } var carpetIndex = {}; var carpetDependents = []; var dataTemplate = (layout.template || {}).data || {}; var templater = Template.traceTemplater(dataTemplate); for(i = 0; i < dataIn.length; i++) { trace = dataIn[i]; // reuse uid we may have pulled out of oldFullData // Note: templater supplies trace type fullTrace = templater.newTrace(trace); fullTrace.uid = fullLayout._traceUids[i]; plots.supplyTraceDefaults(trace, fullTrace, colorCnt, fullLayout, i); fullTrace.index = i; fullTrace._input = trace; fullTrace._expandedIndex = cnt; if(fullTrace.transforms && fullTrace.transforms.length) { var sdInvisible = trace.visible !== false && fullTrace.visible === false; var expandedTraces = applyTransforms(fullTrace, dataOut, layout, fullLayout); for(var j = 0; j < expandedTraces.length; j++) { var expandedTrace = expandedTraces[j]; // No further templating during transforms. var fullExpandedTrace = { _template: fullTrace._template, type: fullTrace.type, // set uid using parent uid and expanded index // to promote consistency between update calls uid: fullTrace.uid + j }; // If the first supplyDefaults created `visible: false`, // clear it before running supplyDefaults a second time, // because sometimes there are items we still want to coerce // inside trace modules before determining that the trace is // again `visible: false`, for example partial visibilities // in `splom` traces. if(sdInvisible && expandedTrace.visible === false) { delete expandedTrace.visible; } plots.supplyTraceDefaults(expandedTrace, fullExpandedTrace, cnt, fullLayout, i); // relink private (i.e. underscore) keys expanded trace to full expanded trace so // that transform supply-default methods can set _ keys for future use. relinkPrivateKeys(fullExpandedTrace, expandedTrace); // add info about parent data trace fullExpandedTrace.index = i; fullExpandedTrace._input = trace; fullExpandedTrace._fullInput = fullTrace; // add info about the expanded data fullExpandedTrace._expandedIndex = cnt; fullExpandedTrace._expandedInput = expandedTrace; pushModule(fullExpandedTrace); } } else { // add identify refs for consistency with transformed traces fullTrace._fullInput = fullTrace; fullTrace._expandedInput = fullTrace; pushModule(fullTrace); } if(Registry.traceIs(fullTrace, 'carpetAxis')) { carpetIndex[fullTrace.carpet] = fullTrace; } if(Registry.traceIs(fullTrace, 'carpetDependent')) { carpetDependents.push(i); } } for(i = 0; i < carpetDependents.length; i++) { fullTrace = dataOut[carpetDependents[i]]; if(!fullTrace.visible) continue; var carpetAxis = carpetIndex[fullTrace.carpet]; fullTrace._carpet = carpetAxis; if(!carpetAxis || !carpetAxis.visible) { fullTrace.visible = false; continue; } fullTrace.xaxis = carpetAxis.xaxis; fullTrace.yaxis = carpetAxis.yaxis; } }; plots.supplyAnimationDefaults = function(opts) { opts = opts || {}; var i; var optsOut = {}; function coerce(attr, dflt) { return Lib.coerce(opts || {}, optsOut, animationAttrs, attr, dflt); } coerce('mode'); coerce('direction'); coerce('fromcurrent'); if(Array.isArray(opts.frame)) { optsOut.frame = []; for(i = 0; i < opts.frame.length; i++) { optsOut.frame[i] = plots.supplyAnimationFrameDefaults(opts.frame[i] || {}); } } else { optsOut.frame = plots.supplyAnimationFrameDefaults(opts.frame || {}); } if(Array.isArray(opts.transition)) { optsOut.transition = []; for(i = 0; i < opts.transition.length; i++) { optsOut.transition[i] = plots.supplyAnimationTransitionDefaults(opts.transition[i] || {}); } } else { optsOut.transition = plots.supplyAnimationTransitionDefaults(opts.transition || {}); } return optsOut; }; plots.supplyAnimationFrameDefaults = function(opts) { var optsOut = {}; function coerce(attr, dflt) { return Lib.coerce(opts || {}, optsOut, animationAttrs.frame, attr, dflt); } coerce('duration'); coerce('redraw'); return optsOut; }; plots.supplyAnimationTransitionDefaults = function(opts) { var optsOut = {}; function coerce(attr, dflt) { return Lib.coerce(opts || {}, optsOut, animationAttrs.transition, attr, dflt); } coerce('duration'); coerce('easing'); return optsOut; }; plots.supplyFrameDefaults = function(frameIn) { var frameOut = {}; function coerce(attr, dflt) { return Lib.coerce(frameIn, frameOut, frameAttrs, attr, dflt); } coerce('group'); coerce('name'); coerce('traces'); coerce('baseframe'); coerce('data'); coerce('layout'); return frameOut; }; plots.supplyTraceDefaults = function(traceIn, traceOut, colorIndex, layout, traceInIndex) { var colorway = layout.colorway || Color.defaults; var defaultColor = colorway[colorIndex % colorway.length]; var i; function coerce(attr, dflt) { return Lib.coerce(traceIn, traceOut, plots.attributes, attr, dflt); } var visible = coerce('visible'); coerce('type'); coerce('name', layout._traceWord + ' ' + traceInIndex); coerce('uirevision', layout.uirevision); // we want even invisible traces to make their would-be subplots visible // so coerce the subplot id(s) now no matter what var _module = plots.getModule(traceOut); traceOut._module = _module; if(_module) { var basePlotModule = _module.basePlotModule; var subplotAttr = basePlotModule.attr; var subplotAttrs = basePlotModule.attributes; if(subplotAttr && subplotAttrs) { var subplots = layout._subplots; var subplotId = ''; if( visible || basePlotModule.name !== 'gl2d' // for now just drop empty gl2d subplots // TODO - currently if we draw an empty gl2d subplot, it draws // nothing then gets stuck and you can't get it back without newPlot // sort this out in the regl refactor? ) { if(Array.isArray(subplotAttr)) { for(i = 0; i < subplotAttr.length; i++) { var attri = subplotAttr[i]; var vali = Lib.coerce(traceIn, traceOut, subplotAttrs, attri); if(subplots[attri]) Lib.pushUnique(subplots[attri], vali); subplotId += vali; } } else { subplotId = Lib.coerce(traceIn, traceOut, subplotAttrs, subplotAttr); } if(subplots[basePlotModule.name]) { Lib.pushUnique(subplots[basePlotModule.name], subplotId); } } } } if(visible) { coerce('customdata'); coerce('ids'); coerce('meta'); if(Registry.traceIs(traceOut, 'showLegend')) { Lib.coerce(traceIn, traceOut, _module.attributes.showlegend ? _module.attributes : plots.attributes, 'showlegend' ); coerce('legend'); coerce('legendwidth'); coerce('legendgroup'); coerce('legendgrouptitle.text'); coerce('legendrank'); traceOut._dfltShowLegend = true; } else { traceOut._dfltShowLegend = false; } if(_module) { _module.supplyDefaults(traceIn, traceOut, defaultColor, layout); } if(!Registry.traceIs(traceOut, 'noOpacity')) { coerce('opacity'); } if(Registry.traceIs(traceOut, 'notLegendIsolatable')) { // This clears out the legendonly state for traces like carpet that // cannot be isolated in the legend traceOut.visible = !!traceOut.visible; } if(!Registry.traceIs(traceOut, 'noHover')) { if(!traceOut.hovertemplate) Lib.coerceHoverinfo(traceIn, traceOut, layout); // parcats support hover, but not hoverlabel stylings (yet) if(traceOut.type !== 'parcats') { Registry.getComponentMethod('fx', 'supplyDefaults')(traceIn, traceOut, defaultColor, layout); } } if(_module && _module.selectPoints) { var selectedpoints = coerce('selectedpoints'); if(Lib.isTypedArray(selectedpoints)) { traceOut.selectedpoints = Array.from(selectedpoints); } } plots.supplyTransformDefaults(traceIn, traceOut, layout); } return traceOut; }; /** * hasMakesDataTransform: does this trace have a transform that makes its own * data, either by grabbing it from somewhere else or by creating it from input * parameters? If so, we should still keep going with supplyDefaults * even if the trace is invisible, which may just be because it has no data yet. */ function hasMakesDataTransform(trace) { var transforms = trace.transforms; if(Array.isArray(transforms) && transforms.length) { for(var i = 0; i < transforms.length; i++) { var ti = transforms[i]; var _module = ti._module || transformsRegistry[ti.type]; if(_module && _module.makesData) return true; } } return false; } plots.hasMakesDataTransform = hasMakesDataTransform; plots.supplyTransformDefaults = function(traceIn, traceOut, layout) { // For now we only allow transforms on 1D traces, ie those that specify a _length. // If we were to implement 2D transforms, we'd need to have each transform // describe its own applicability and disable itself when it doesn't apply. // Also allow transforms that make their own data, but not in globalTransforms if(!(traceOut._length || hasMakesDataTransform(traceIn))) return; var globalTransforms = layout._globalTransforms || []; var transformModules = layout._transformModules || []; if(!Array.isArray(traceIn.transforms) && globalTransforms.length === 0) return; var containerIn = traceIn.transforms || []; var transformList = globalTransforms.concat(containerIn); var containerOut = traceOut.transforms = []; for(var i = 0; i < transformList.length; i++) { var transformIn = transformList[i]; var type = transformIn.type; var _module = transformsRegistry[type]; var transformOut; /* * Supply defaults may run twice. First pass runs all supply defaults steps * and adds the _module to any output transforms. * If transforms exist another pass is run so that any generated traces also * go through supply defaults. This has the effect of rerunning * supplyTransformDefaults. If the transform does not have a `transform` * function it could not have generated any new traces and the second stage * is unnecessary. We detect this case with the following variables. */ var isFirstStage = !(transformIn._module && transformIn._module === _module); var doLaterStages = _module && typeof _module.transform === 'function'; if(!_module) Lib.warn('Unrecognized transform type ' + type + '.'); if(_module && _module.supplyDefaults && (isFirstStage || doLaterStages)) { transformOut = _module.supplyDefaults(transformIn, traceOut, layout, traceIn); transformOut.type = type; transformOut._module = _module; Lib.pushUnique(transformModules, _module); } else { transformOut = Lib.extendFlat({}, transformIn); } containerOut.push(transformOut); } }; function applyTransforms(fullTrace, fullData, layout, fullLayout) { var container = fullTrace.transforms; var dataOut = [fullTrace]; for(var i = 0; i < container.length; i++) { var transform = container[i]; var _module = transformsRegistry[transform.type]; if(_module && _module.transform) { dataOut = _module.transform(dataOut, { transform: transform, fullTrace: fullTrace, fullData: fullData, layout: layout, fullLayout: fullLayout, transformIndex: i }); } } return dataOut; } plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut, formatObj) { function coerce(attr, dflt) { return Lib.coerce(layoutIn, layoutOut, plots.layoutAttributes, attr, dflt); } var template = layoutIn.template; if(Lib.isPlainObject(template)) { layoutOut.template = template; layoutOut._template = template.layout; layoutOut._dataTemplate = template.data; } coerce('autotypenumbers'); var font = Lib.coerceFont(coerce, 'font'); var fontSize = font.size; Lib.coerceFont(coerce, 'title.font', font, { overrideDflt: { size: Math.round(fontSize * 1.4) }}); coerce('title.text', layoutOut._dfltTitle.plot); coerce('title.xref'); var titleYref = coerce('title.yref'); coerce('title.pad.t'); coerce('title.pad.r'); coerce('title.pad.b'); coerce('title.pad.l'); var titleAutomargin = coerce('title.automargin'); coerce('title.x'); coerce('title.xanchor'); coerce('title.y'); coerce('title.yanchor'); coerce('title.subtitle.text', layoutOut._dfltTitle.subtitle); Lib.coerceFont(coerce, 'title.subtitle.font', font, { overrideDflt: { size: Math.round(layoutOut.title.font.size * 0.7) } }); if(titleAutomargin) { // when automargin=true // title.y is 1 or 0 if paper ref // 'auto' is not supported for either title.y or title.yanchor // TODO: mention this smart default in the title.y and title.yanchor descriptions if(titleYref === 'paper') { if(layoutOut.title.y !== 0) layoutOut.title.y = 1; if(layoutOut.title.yanchor === 'auto') { layoutOut.title.yanchor = layoutOut.title.y === 0 ? 'top' : 'bottom'; } } if(titleYref === 'container') { if(layoutOut.title.y === 'auto') layoutOut.title.y = 1; if(layoutOut.title.yanchor === 'auto') { layoutOut.title.yanchor = layoutOut.title.y < 0.5 ? 'bottom' : 'top'; } } } var uniformtextMode = coerce('uniformtext.mode'); if(uniformtextMode) { coerce('uniformtext.minsize'); } // Make sure that autosize is defaulted to *true* // on layouts with no set width and height for backward compatibly, // in particular https://plotly.com/javascript/responsive-fluid-layout/ // // Before https://github.com/plotly/plotly.js/pull/635 , // layouts with no set width and height were set temporary set to 'initial' // to pass through the autosize routine // // This behavior is subject to change in v3. coerce('autosize', !(layoutIn.width && layoutIn.height)); coerce('width'); coerce('height'); coerce('minreducedwidth'); coerce('minreducedheight'); coerce('margin.l'); coerce('margin.r'); coerce('margin.t'); coerce('margin.b'); coerce('margin.pad'); coerce('margin.autoexpand'); if(layoutIn.width && layoutIn.height) plots.sanitizeMargins(layoutOut); Registry.getComponentMethod('grid', 'sizeDefaults')(layoutIn, layoutOut); coerce('paper_bgcolor'); coerce('separators', formatObj.decimal + formatObj.thousands); coerce('hidesources'); coerce('colorway'); coerce('datarevision'); var uirevision = coerce('uirevision'); coerce('editrevision', uirevision); coerce('selectionrevision', uirevision); Registry.getComponentMethod( 'modebar', 'supplyLayoutDefaults' )(layoutIn, layoutOut); Registry.getComponentMethod( 'shapes', 'supplyDrawNewShapeDefaults' )(layoutIn, layoutOut, coerce); Registry.getComponentMethod( 'selections', 'supplyDrawNewSelectionDefaults' )(layoutIn, layoutOut, coerce); coerce('meta'); // do not include defaults in fullLayout when users do not set transition if(Lib.isPlainObject(layoutIn.transition)) { coerce('transition.duration'); coerce('transition.easing'); coerce('transition.ordering'); } Registry.getComponentMethod( 'calendars', 'handleDefaults' )(layoutIn, layoutOut, 'calendar'); Registry.getComponentMethod( 'fx', 'supplyLayoutGlobalDefaults' )(layoutIn, layoutOut, coerce); Lib.coerce(layoutIn, layoutOut, scatterAttrs, 'scattermode'); }; function getComputedSize(attr) { return ( (typeof attr === 'string') && (attr.substr(attr.length - 2) === 'px') && parseFloat(attr) ); } plots.plotAutoSize = function plotAutoSize(gd, layout, fullLayout) { var context = gd._context || {}; var frameMargins = context.frameMargins; var newWidth; var newHeight; var isPlotDiv = Lib.isPlotDiv(gd); if(isPlotDiv) gd.emit('plotly_autosize'); // embedded in an iframe - just take the full iframe size // if we get to this point, with no aspect ratio restrictions if(context.fillFrame) { newWidth = window.innerWidth; newHeight = window.innerHeight; // somehow we get a few extra px height sometimes... // just hide it document.body.style.overflow = 'hidden'; } else { // plotly.js - let the developers do what they want, either // provide height and width for the container div, // specify size in layout, or take the defaults, // but don't enforce any ratio restrictions var computedStyle = isPlotDiv ? window.getComputedStyle(gd) : {}; newWidth = getComputedSize(computedStyle.width) || getComputedSize(computedStyle.maxWidth) || fullLayout.width; newHeight = getComputedSize(computedStyle.height) || getComputedSize(computedStyle.maxHeight) || fullLayout.height; if(isNumeric(frameMargins) && frameMargins > 0) { var factor = 1 - 2 * frameMargins; newWidth = Math.round(factor * newWidth); newHeight = Math.round(factor * newHeight); } } var minWidth = plots.layoutAttributes.width.min; var minHeight = plots.layoutAttributes.height.min; if(newWidth < minWidth) newWidth = minWidth; if(newHeight < minHeight) newHeight = minHeight; var widthHasChanged = !layout.width && (Math.abs(fullLayout.width - newWidth) > 1); var heightHasChanged = !layout.height && (Math.abs(fullLayout.height - newHeight) > 1); if(heightHasChanged || widthHasChanged) { if(widthHasChanged) fullLayout.width = newWidth; if(heightHasChanged) fullLayout.height = newHeight; } // cache initial autosize value, used in relayout when // width or height values are set to null if(!gd._initialAutoSize) { gd._initialAutoSize = { width: newWidth, height: newHeight }; } plots.sanitizeMargins(fullLayout); }; plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData, transitionData) { var componentsRegistry = Registry.componentsRegistry; var basePlotModules = layoutOut._basePlotModules; var component, i, _module; var Cartesian = Registry.subplotsRegistry.cartesian; // check if any components need to add more base plot modules // that weren't captured by traces for(component in componentsRegistry) { _module = componentsRegistry[component]; if(_module.includeBasePlot) { _module.includeBasePlot(layoutIn, layoutOut); } } // make sure we *at least* have some cartesian axes if(!basePlotModules.length) { basePlotModules.push(Cartesian); } // ensure all cartesian axes have at least one subplot if(layoutOut._has('cartesian')) { Registry.getComponentMethod('grid', 'contentDefaults')(layoutIn, layoutOut); Cartesian.finalizeSubplots(layoutIn, layoutOut); } // sort subplot lists for(var subplotType in layoutOut._subplots) { layoutOut._subplots[subplotType].sort(Lib.subplotSort); } // base plot module layout defaults for(i = 0; i < basePlotModules.length; i++) { _module = basePlotModules[i]; // e.g. pie does not have a layout-defaults step if(_module.supplyLayoutDefaults) { _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData); } } // trace module layout defaults // use _modules rather than _visibleModules so that even // legendonly traces can include settings - eg barmode, which affects // legend.traceorder default value. var modules = layoutOut._modules; for(i = 0; i < modules.length; i++) { _module = modules[i]; if(_module.supplyLayoutDefaults) { _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData); } } // transform module layout defaults var transformModules = layoutOut._transformModules; for(i = 0; i < transformModules.length; i++) { _module = transformModules[i]; if(_module.supplyLayoutDefaults) { _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData, transitionData); } } for(component in componentsRegistry) { _module = componentsRegistry[component]; if(_module.supplyLayoutDefaults) { _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData); } } }; // Remove all plotly attributes from a div so it can be replotted fresh // TODO: these really need to be encapsulated into a much smaller set... plots.purge = function(gd) { // note: we DO NOT remove _context because it doesn't change when we insert // a new plot, and may have been set outside of our scope. var fullLayout = gd._fullLayout || {}; if(fullLayout._glcontainer !== undefined) { fullLayout._glcontainer.selectAll('.gl-canvas').remove(); fullLayout._glcontainer.remove(); fullLayout._glcanvas = null; } // remove modebar if(fullLayout._modeBar) fullLayout._modeBar.destroy(); if(gd._transitionData) { // Ensure any dangling callbacks are simply dropped if the plot is purged. // This is more or less only actually important for testing. if(gd._transitionData._interruptCallbacks) { gd._transitionData._interruptCallbacks.length = 0; } if(gd._transitionData._animationRaf) { window.cancelAnimationFrame(gd._transitionData._animationRaf); } } // remove any planned throttles Lib.clearThrottle(); // remove responsive handler Lib.clearResponsive(gd); // data and layout delete gd.data; delete gd.layout; delete gd._fullData; delete gd._fullLayout; delete gd.calcdata; delete gd.empty; delete gd.fid; delete gd.undoqueue; // action queue delete gd.undonum; delete gd.autoplay; // are we doing an action that doesn't go in undo queue? delete gd.changed; // these get recreated on _doPlot anyway, but just to be safe // (and to have a record of them...) delete gd._promises; delete gd._redrawTimer; delete gd._hmlumcount; delete gd._hmpixcount; delete gd._transitionData; delete gd._transitioning; delete gd._initialAutoSize; delete gd._transitioningWithDuration; // created during certain events, that *should* clean them up // themselves, but may not if there was an error delete gd._dragging; delete gd._dragged; delete gd._dragdata; delete gd._hoverdata; delete gd._snapshotInProgress; delete gd._editing; delete gd._mouseDownTime; delete gd._legendMouseDownTime; // remove all event listeners if(gd.removeAllListeners) gd.removeAllListeners(); }; plots.style = function(gd) { var _modules = gd._fullLayout._visibleModules; var styleModules = []; var i; // some trace modules reuse the same style method, // make sure to not unnecessary call them multiple times. for(i = 0; i < _modules.length; i++) { var _module = _modules[i]; if(_module.style) { Lib.pushUnique(styleModules, _module.style); } } for(i = 0; i < styleModules.length; i++) { styleModules[i](gd); } }; plots.sanitizeMargins = function(fullLayout) { // polar doesn't do margins... if(!fullLayout || !fullLayout.margin) return; var width = fullLayout.width; var height = fullLayout.height; var margin = fullLayout.margin; var plotWidth = width - (margin.l + margin.r); var plotHeight = height - (margin.t + margin.b); var correction; // if margin.l + margin.r = 0 then plotWidth > 0 // as width >= 10 by supplyDefaults // similarly for margin.t + margin.b if(plotWidth < 0) { correction = (width - 1) / (margin.l + margin.r); margin.l = Math.floor(correction * margin.l); margin.r = Math.floor(correction * margin.r); } if(plotHeight < 0) { correction = (height - 1) / (margin.t + margin.b); margin.t = Math.floor(correction * margin.t); margin.b = Math.floor(correction * margin.b); } }; plots.clearAutoMarginIds = function(gd) { gd._fullLayout._pushmarginIds = {}; }; plots.allowAutoMargin = function(gd, id) { gd._fullLayout._pushmarginIds[id] = 1; }; function initMargins(fullLayout) { var margin = fullLayout.margin; if(!fullLayout._size) { var gs = fullLayout._size = { l: Math.round(margin.l), r: Math.round(margin.r), t: Math.round(margin.t), b: Math.round(margin.b), p: Math.round(margin.pad) }; gs.w = Math.round(fullLayout.width) - gs.l - gs.r; gs.h = Math.round(fullLayout.height) - gs.t - gs.b; } if(!fullLayout._pushmargin) fullLayout._pushmargin = {}; if(!fullLayout._pushmarginIds) fullLayout._pushmarginIds = {}; if(!fullLayout._reservedMargin) fullLayout._reservedMargin = {}; } // non-negotiable - this is the smallest height we will allow users to specify via explicit margins var MIN_SPECIFIED_WIDTH = 2; var MIN_SPECIFIED_HEIGHT = 2; /** * autoMargin: called by components that may need to expand the margins to * be rendered on-plot. * * @param {DOM element} gd * @param {string} id - an identifier unique (within this plot) to this object, * so we can remove a previous margin expansion from the same object. * @param {object} o - the margin requirements of this object, or omit to delete * this entry (like if it's hidden). Keys are: * x, y: plot fraction of the anchor point. * xl, xr, yt, yb: if the object has an extent defined in plot fraction, * you can specify both edges as plot fractions in each dimension * l, r, t, b: the pixels to pad past the plot fraction x[l|r] and y[t|b] * pad: extra pixels to add in all directions, default 12 (why?) */ plots.autoMargin = function(gd, id, o) { var fullLayout = gd._fullLayout; var width = fullLayout.width; var height = fullLayout.height; var margin = fullLayout.margin; var minreducedwidth = fullLayout.minreducedwidth; var minreducedheight = fullLayout.minreducedheight; var minFinalWidth = Lib.constrain( width - margin.l - margin.r, MIN_SPECIFIED_WIDTH, minreducedwidth ); var minFinalHeight = Lib.constrain( height - margin.t - margin.b, MIN_SPECIFIED_HEIGHT, minreducedheight ); var maxSpaceW = Math.max(0, width - minFinalWidth); var maxSpaceH = Math.max(0, height - minFinalHeight); var pushMargin = fullLayout._pushmargin; var pushMarginIds = fullLayout._pushmarginIds; if(margin.autoexpand !== false) { if(!o) { delete pushMargin[id]; delete pushMarginIds[id]; } else { var pad = o.pad; if(pad === undefined) { // if no explicit pad is given, use 12px unless there's a // specified margin that's smaller than that pad = Math.min(12, margin.l, margin.r, margin.t, margin.b); } // if the item is too big, just give it enough automargin to // make sure you can still grab it and bring it back if(maxSpaceW) { var rW = (o.l + o.r) / maxSpaceW; if(rW > 1) { o.l /= rW; o.r /= rW; } } if(maxSpaceH) { var rH = (o.t + o.b) / maxSpaceH; if(rH > 1) { o.t /= rH; o.b /= rH; } } var xl = o.xl !== undefined ? o.xl : o.x; var xr = o.xr !== undefined ? o.xr : o.x; var yt = o.yt !== undefined ? o.yt : o.y; var yb = o.yb !== undefined ? o.yb : o.y; pushMargin[id] = { l: {val: xl, size: o.l + pad}, r: {val: xr, size: o.r + pad}, b: {val: yb, size: o.b + pad}, t: {val: yt, size: o.t + pad} }; pushMarginIds[id] = 1; } if(!fullLayout._replotting) { return plots.doAutoMargin(gd); } } }; function needsRedrawForShift(gd) { if('_redrawFromAutoMarginCount' in gd._fullLayout) { return false; } var axList = axisIDs.list(gd, '', true); for(var ax in axList) { if(axList[ax].autoshift || axList[ax].shift) return true; } return false; } plots.doAutoMargin = function(gd) { var fullLayout = gd._fullLayout; var width = fullLayout.width; var height = fullLayout.height; if(!fullLayout._size) fullLayout._size = {}; initMargins(fullLayout); var gs = fullLayout._size; var margin = fullLayout.margin; var reservedMargins = {t: 0, b: 0, l: 0, r: 0}; var oldMargins = Lib.extendFlat({}, gs); // adjust margins for outside components // fullLayout.margin is the requested margin, // fullLayout._size has margins and plotsize after adjustment var ml = margin.l; var mr = margin.r; var mt = margin.t; var mb = margin.b; var pushMargin = fullLayout._pushmargin; var pushMarginIds = fullLayout._pushmarginIds; var minreducedwidth = fullLayout.minreducedwidth; var minreducedheight = fullLayout.minreducedheight; if(margin.autoexpand !== false) { for(var k in pushMargin) { if(!pushMarginIds[k]) delete pushMargin[k]; } var margins = gd._fullLayout._reservedMargin; for(var key in margins) { for(var side in margins[key]) { var val = margins[key][side]; reservedMargins[side] = Math.max(reservedMargins[side], val); } } // fill in the requested margins pushMargin.base = { l: {val: 0, size: ml}, r: {val: 1, size: mr}, t: {val: 1, size: mt}, b: {val: 0, size: mb} }; // make sure that the reservedMargin is the minimum needed for(var s in reservedMargins) { var autoMarginPush = 0; for(var m in pushMargin) { if(m !== 'base') { if(isNumeric(pushMargin[m][s].size)) { autoMarginPush = pushMargin[m][s].size > autoMarginPush ? pushMargin[m][s].size : autoMarginPush; } } } var extraMargin = Math.max(0, (margin[s] - autoMarginPush)); reservedMargins[s] = Math.max(0, reservedMargins[s] - extraMargin); } // now cycle through all the combinations of l and r // (and t and b) to find the required margins for(var k1 in pushMargin) { var pushleft = pushMargin[k1].l || {}; var pushbottom = pushMargin[k1].b || {}; var fl = pushleft.val; var pl = pushleft.size; var fb = pushbottom.val; var pb = pushbottom.size; var availableWidth = width - reservedMargins.r - reservedMargins.l; var availableHeight = height - reservedMargins.t - reservedMargins.b; for(var k2 in pushMargin) { if(isNumeric(pl) && pushMargin[k2].r) { var fr = pushMargin[k2].r.val; var pr = pushMargin[k2].r.size; if(fr > fl) { var newL = (pl * fr + (pr - availableWidth) * fl) / (fr - fl); var newR = (pr * (1 - fl) + (pl - availableWidth) * (1 - fr)) / (fr - fl); if(newL + newR > ml + mr) { ml = newL; mr = newR; } } } if(isNumeric(pb) && pushMargin[k2].t) { var ft = pushMargin[k2].t.val; var pt = pushMargin[k2].t.size; if(ft > fb) { var newB = (pb * ft + (pt - availableHeight) * fb) / (ft - fb); var newT = (pt * (1 - fb) + (pb - availableHeight) * (1 - ft)) / (ft - fb); if(newB + newT > mb + mt) { mb = newB; mt = newT; } } } } } } var minFinalWidth = Lib.constrain( width - margin.l - margin.r, MIN_SPECIFIED_WIDTH, minreducedwidth ); var minFinalHeight = Lib.constrain( height - margin.t - margin.b, MIN_SPECIFIED_HEIGHT, minreducedheight ); var maxSpaceW = Math.max(0, width - minFinalWidth); var maxSpaceH = Math.max(0, height - minFinalHeight); if(maxSpaceW) { var rW = (ml + mr) / maxSpaceW; if(rW > 1) { ml /= rW; mr /= rW; } } if(maxSpaceH) { var rH = (mb + mt) / maxSpaceH; if(rH > 1) { mb /= rH; mt /= rH; } } gs.l = Math.round(ml) + reservedMargins.l; gs.r = Math.round(mr) + reservedMargins.r; gs.t = Math.round(mt) + reservedMargins.t; gs.b = Math.round(mb) + reservedMargins.b; gs.p = Math.round(margin.pad); gs.w = Math.round(width) - gs.l - gs.r; gs.h = Math.round(height) - gs.t - gs.b; // if things changed and we're not already redrawing, trigger a redraw if(!fullLayout._replotting && (plots.didMarginChange(oldMargins, gs) || needsRedrawForShift(gd))) { if('_redrawFromAutoMarginCount' in fullLayout) { fullLayout._redrawFromAutoMarginCount++; } else { fullLayout._redrawFromAutoMarginCount = 1; } // Always allow at least one redraw and give each margin-push // call 3 loops to converge. Of course, for most cases this way too many, // but let's keep things on the safe side until we fix our // auto-margin pipeline problems: // https://github.com/plotly/plotly.js/issues/2704 var maxNumberOfRedraws = 3 * (1 + Object.keys(pushMarginIds).length); if(fullLayout._redrawFromAutoMarginCount < maxNumberOfRedraws) { return Registry.call('_doPlot', gd); } else { fullLayout._size = oldMargins; Lib.warn('Too many auto-margin redraws.'); } } refineTicks(gd); }; function refineTicks(gd) { var axList = axisIDs.list(gd, '', true); [ '_adjustTickLabelsOverflow', '_hideCounterAxisInsideTickLabels' ].forEach(function(k) { for(var i = 0; i < axList.length; i++) { var hideFn = axList[i][k]; if(hideFn) hideFn(); } }); } var marginKeys = ['l', 'r', 't', 'b', 'p', 'w', 'h']; plots.didMarginChange = function(margin0, margin1) { for(var i = 0; i < marginKeys.length; i++) { var k = marginKeys[i]; var m0 = margin0[k]; var m1 = margin1[k]; // use 1px tolerance in case we old/new differ only // by rounding errors, which can lead to infinite loops if(!isNumeric(m0) || Math.abs(m1 - m0) > 1) { return true; } } return false; }; /** * JSONify the graph data and layout * * This function needs to recurse because some src can be inside * sub-objects. * * It also strips out functions and private (starts with _) elements. * Therefore, we can add temporary things to data and layout that don't * get saved. * * @param gd The graphDiv * @param {Boolean} dataonly If true, don't return layout. * @param {'keepref'|'keepdata'|'keepall'} [mode='keepref'] Filter what's kept * keepref: remove data for which there's a src present * eg if there's xsrc present (and xsrc is well-formed, * ie has : and some chars before it), strip out x * keepdata: remove all src tags, don't remove the data itself * keepall: keep data and src * @param {String} output If you specify 'object', the result will not be stringified * @param {Boolean} useDefaults If truthy, use _fullLayout and _fullData * @param {Boolean} includeConfig If truthy, include _context * @returns {Object|String} */ plots.graphJson = function(gd, dataonly, mode, output, useDefaults, includeConfig) { // if the defaults aren't supplied yet, we need to do that... if((useDefaults && dataonly && !gd._fullData) || (useDefaults && !dataonly && !gd._fullLayout)) { plots.supplyDefaults(gd); } var data = (useDefaults) ? gd._fullData : gd.data; var layout = (useDefaults) ? gd._fullLayout : gd.layout; var frames = (gd._transitionData || {})._frames; function stripObj(d, keepFunction) { if(typeof d === 'function') { return keepFunction ? '_function_' : null; } if(Lib.isPlainObject(d)) { var o = {}; var src; Object.keys(d).sort().forEach(function(v) { // remove private elements and functions // _ is for private, [ is a mistake ie [object Object] if(['_', '['].indexOf(v.charAt(0)) !== -1) return; // if a function, add if necessary then move on if(typeof d[v] === 'function') { if(keepFunction) o[v] = '_function'; return; } // look for src/data matches and remove the appropriate one if(mode === 'keepdata') { // keepdata: remove all ...src tags if(v.substr(v.length - 3) === 'src') { return; } } else if(mode === 'keepstream') { // keep sourced data if it's being streamed. // similar to keepref, but if the 'stream' object exists // in a trace, we will keep the data array. src = d[v + 'src']; if(typeof src === 'string' && src.indexOf(':') > 0) { if(!Lib.isPlainObject(d.stream)) { return; } } } else if(mode !== 'keepall') { // keepref: remove sourced data but only // if the source tag is well-formed src = d[v + 'src']; if(typeof src === 'string' && src.indexOf(':') > 0) { return; } } // OK, we're including this... recurse into it o[v] = stripObj(d[v], keepFunction); }); return o; } var dIsArray = Array.isArray(d); var dIsTypedArray = Lib.isTypedArray(d); if((dIsArray || dIsTypedArray) && d.dtype && d.shape) { var bdata = d.bdata; return stripObj({ dtype: d.dtype, shape: d.shape, bdata: // case of ArrayBuffer Lib.isArrayBuffer(bdata) ? b64encode.encode(bdata) : // case of b64 string bdata }, keepFunction); } if(dIsArray) { return d.map(function(x) {return stripObj(x, keepFunction);}); } if(dIsTypedArray) { return Lib.simpleMap(d, Lib.identity); } // convert native dates to date strings... // mostly for external users exporting to plotly if(Lib.isJSDate(d)) return Lib.ms2DateTimeLocal(+d); return d; } var obj = { data: (data || []).map(function(v) { var d = stripObj(v); // fit has some little arrays in it that don't contain data, // just fit params and meta if(dataonly) { delete d.fit; } return d; }) }; if(!dataonly) { obj.layout = stripObj(layout); if(useDefaults) { var gs = layout._size; obj.layout.computed = { margin: { b: gs.b, l: gs.l, r: gs.r, t: gs.t } }; } } if(frames) obj.frames = stripObj(frames); if(includeConfig) obj.config = stripObj(gd._context, true); return (output === 'object') ? obj : JSON.stringify(obj); }; /** * Modify a keyframe using a list of operations: * * @param {array of objects} operations * Sequence of operations to be performed on the keyframes */ plots.modifyFrames = function(gd, operations) { var i, op, frame; var _frames = gd._transitionData._frames; var _frameHash = gd._transitionData._frameHash; for(i = 0; i < operations.length; i++) { op = operations[i]; switch(op.type) { // No reason this couldn't exist, but is currently unused/untested: /* case 'rename': frame = _frames[op.index]; delete _frameHash[frame.name]; _frameHash[op.name] = frame; frame.name = op.name; break;*/ case 'replace': frame = op.value; var oldName = (_frames[op.index] || {}).name; var newName = frame.name; _frames[op.index] = _frameHash[newName] = frame; if(newName !== oldName) { // If name has changed in addition to replacement, then update // the lookup table: delete _frameHash[oldName]; _frameHash[newName] = frame; } break; case 'insert': frame = op.value; _frameHash[frame.name] = frame; _frames.splice(op.index, 0, frame); break; case 'delete': frame = _frames[op.index]; delete _frameHash[frame.name]; _frames.splice(op.index, 1); break; } } return Promise.resolve(); }; /* * Compute a keyframe. Merge a keyframe into its base frame(s) and * expand properties. * * @param {object} frameLookup * An object containing frames keyed by name (i.e. gd._transitionData._frameHash) * @param {string} frame * The name of the keyframe to be computed * * Returns: a new object with the merged content */ plots.computeFrame = function(gd, frameName) { var frameLookup = gd._transitionData._frameHash; var i, traceIndices, traceIndex, destIndex; // Null or undefined will fail on .toString(). We'll allow numbers since we // make it clear frames must be given string names, but we'll allow numbers // here since they're otherwise fine for looking up frames as long as they're // properly cast to strings. We really just want to ensure here that this // 1) doesn't fail, and // 2) doens't give an incorrect answer (which String(frameName) would) if(!frameName) { throw new Error('computeFrame must be given a string frame name'); } var framePtr = frameLookup[frameName.toString()]; // Return false if the name is invalid: if(!framePtr) { return false; } var frameStack = [framePtr]; var frameNameStack = [framePtr.name]; // Follow frame pointers: while(framePtr.baseframe && (framePtr = frameLookup[framePtr.baseframe.toString()])) { // Avoid infinite loops: if(frameNameStack.indexOf(framePtr.name) !== -1) break; frameStack.push(framePtr); frameNameStack.push(framePtr.name); } // A new object for the merged result: var result = {}; // Merge, starting with the last and ending with the desired frame: while((framePtr = frameStack.pop())) { if(framePtr.layout) { result.layout = plots.extendLayout(result.layout, framePtr.layout); } if(framePtr.data) { if(!result.data) { result.data = []; } traceIndices = framePtr.traces; if(!traceIndices) { // If not defined, assume serial order starting at zero traceIndices = []; for(i = 0; i < framePtr.data.length; i++) { traceIndices[i] = i; } } if(!result.traces) { result.traces = []; } for(i = 0; i < framePtr.data.length; i++) { // Loop through this frames data, find out where it should go, // and merge it! traceIndex = traceIndices[i]; if(traceIndex === undefined || traceIndex === null) { continue; } destIndex = result.traces.indexOf(traceIndex); if(destIndex === -1) { destIndex = result.data.length; result.traces[destIndex] = traceIndex; } result.data[destIndex] = plots.extendTrace(result.data[destIndex], framePtr.data[i]); } } } return result; }; /* * Recompute the lookup table that maps frame name -> frame object. addFrames/ * deleteFrames already manages this data one at a time, so the only time this * is necessary is if you poke around manually in `gd._transitionData._frames` * and create and haven't updated the lookup table. */ plots.recomputeFrameHash = function(gd) { var hash = gd._transitionData._frameHash = {}; var frames = gd._transitionData._frames; for(var i = 0; i < frames.length; i++) { var frame = frames[i]; if(frame && frame.name) { hash[frame.name] = frame; } } }; /** * Extend an object, treating container arrays very differently by extracting * their contents and merging them separately. * * This exists so that we can extendDeepNoArrays and avoid stepping into data * arrays without knowledge of the plot schema, but so that we may also manually * recurse into known container arrays, such as transforms. * * See extendTrace and extendLayout below for usage. */ plots.extendObjectWithContainers = function(dest, src, containerPaths) { var containerProp, containerVal, i, j, srcProp, destProp, srcContainer, destContainer; var copy = Lib.extendDeepNoArrays({}, src || {}); var expandedObj = Lib.expandObjectPaths(copy); var containerObj = {}; // Step through and extract any container properties. Otherwise extendDeepNoArrays // will clobber any existing properties with an empty array and then supplyDefaults // will reset everything to defaults. if(containerPaths && containerPaths.length) { for(i = 0; i < containerPaths.length; i++) { containerProp = Lib.nestedProperty(expandedObj, containerPaths[i]); containerVal = containerProp.get(); if(containerVal === undefined) { Lib.nestedProperty(containerObj, containerPaths[i]).set(null); } else { containerProp.set(null); Lib.nestedProperty(containerObj, containerPaths[i]).set(containerVal); } } } dest = Lib.extendDeepNoArrays(dest || {}, expandedObj); if(containerPaths && containerPaths.length) { for(i = 0; i < containerPaths.length; i++) { srcProp = Lib.nestedProperty(containerObj, containerPaths[i]); srcContainer = srcProp.get(); if(!srcContainer) continue; destProp = Lib.nestedProperty(dest, containerPaths[i]); destContainer = destProp.get(); if(!Array.isArray(destContainer)) { destContainer = []; destProp.set(destContainer); } for(j = 0; j < srcContainer.length; j++) { var srcObj = srcContainer[j]; if(srcObj === null) destContainer[j] = null; else { destContainer[j] = plots.extendObjectWithContainers(destContainer[j], srcObj); } } destProp.set(destContainer); } } return dest; }; plots.dataArrayContainers = ['transforms', 'dimensions']; plots.layoutArrayContainers = Registry.layoutArrayContainers; /* * Extend a trace definition. This method: * * 1. directly transfers any array references * 2. manually recurses into container arrays like transforms * * The result is the original object reference with the new contents merged in. */ plots.extendTrace = function(destTrace, srcTrace) { return plots.extendObjectWithContainers(destTrace, srcTrace, plots.dataArrayContainers); }; /* * Extend a layout definition. This method: * * 1. directly transfers any array references (not critically important for * layout since there aren't really data arrays) * 2. manually recurses into container arrays like annotations * * The result is the original object reference with the new contents merged in. */ plots.extendLayout = function(destLayout, srcLayout) { return plots.extendObjectWithContainers(destLayout, srcLayout, plots.layoutArrayContainers); }; /** * Transition to a set of new data and layout properties from Plotly.animate * * @param {DOM element} gd * @param {Object[]} data * an array of data objects following the normal Plotly data definition format * @param {Object} layout * a layout object, following normal Plotly layout format * @param {Number[]} traces * indices of the corresponding traces specified in `data` * @param {Object} frameOpts * options for the frame (i.e. whether to redraw post-transition) * @param {Object} transitionOpts * options for the transition */ plots.transition = function(gd, data, layout, traces, frameOpts, transitionOpts) { var opts = {redraw: frameOpts.redraw}; var transitionedTraces = {}; var axEdits = []; opts.prepareFn = function() { var dataLength = Array.isArray(data) ? data.length : 0; var traceIndices = traces.slice(0, dataLength); for(var i = 0; i < traceIndices.length; i++) { var traceIdx = traceIndices[i]; var trace = gd._fullData[traceIdx]; var _module = trace._module; // There's nothing to do if this module is not defined: if(!_module) continue; // Don't register the trace as transitioned if it doesn't know what to do. // If it *is* registered, it will receive a callback that it's responsible // for calling in order to register the transition as having completed. if(_module.animatable) { var n = _module.basePlotModule.name; if(!transitionedTraces[n]) transitionedTraces[n] = []; transitionedTraces[n].push(traceIdx); } gd.data[traceIndices[i]] = plots.extendTrace(gd.data[traceIndices[i]], data[i]); } // Follow the same procedure. Clone it so we don't mangle the input, then // expand any object paths so we can merge deep into gd.layout: var layoutUpdate = Lib.expandObjectPaths(Lib.extendDeepNoArrays({}, layout)); // Before merging though, we need to modify the incoming layout. We only // know how to *transition* layout ranges, so it's imperative that a new // range not be sent to the layout before the transition has started. So // we must remove the things we can transition: var axisAttrRe = /^[xy]axis[0-9]*$/; for(var attr in layoutUpdate) { if(!axisAttrRe.test(attr)) continue; delete layoutUpdate[attr].range; } plots.extendLayout(gd.layout, layoutUpdate); // Supply defaults after applying the incoming properties. Note that any attempt // to simplify this step and reduce the amount of work resulted in the reconstruction // of essentially the whole supplyDefaults step, so that it seems sensible to just use // supplyDefaults even though it's heavier than would otherwise be desired for // transitions: // first delete calcdata so supplyDefaults knows a calc step is coming delete gd.calcdata; plots.supplyDefaults(gd); plots.doCalcdata(gd); var newLayout = Lib.expandObjectPaths(layout); if(newLayout) { var subplots = gd._fullLayout._plots; for(var k in subplots) { var plotinfo = subplots[k]; var xa = plotinfo.xaxis; var ya = plotinfo.yaxis; var xr0 = xa.range.slice(); var yr0 = ya.range.slice(); var xr1 = null; var yr1 = null; var editX = null; var editY = null; if(Array.isArray(newLayout[xa._name + '.range'])) { xr1 = newLayout[xa._name + '.range'].slice(); } else if(Array.isArray((newLayout[xa._name] || {}).range)) { xr1 = newLayout[xa._name].range.slice(); } if(Array.isArray(newLayout[ya._name + '.range'])) { yr1 = newLayout[ya._name + '.range'].slice(); } else if(Array.isArray((newLayout[ya._name] || {}).range)) { yr1 = newLayout[ya._name].range.slice(); } if(xr0 && xr1 && (xa.r2l(xr0[0]) !== xa.r2l(xr1[0]) || xa.r2l(xr0[1]) !== xa.r2l(xr1[1])) ) { editX = {xr0: xr0, xr1: xr1}; } if(yr0 && yr1 && (ya.r2l(yr0[0]) !== ya.r2l(yr1[0]) || ya.r2l(yr0[1]) !== ya.r2l(yr1[1])) ) { editY = {yr0: yr0, yr1: yr1}; } if(editX || editY) { axEdits.push(Lib.extendFlat({plotinfo: plotinfo}, editX, editY)); } } } return Promise.resolve(); }; opts.runFn = function(makeCallback) { var traceTransitionOpts; var basePlotModules = gd._fullLayout._basePlotModules; var hasAxisTransition = axEdits.length; var i; if(layout) { for(i = 0; i < basePlotModules.length; i++) { if(basePlotModules[i].transitionAxes) { basePlotModules[i].transitionAxes(gd, axEdits, transitionOpts, makeCallback); } } } // Here handle the exception that we refuse to animate scales and axes at the same // time. In other words, if there's an axis transition, then set the data transition // to instantaneous. if(hasAxisTransition) { traceTransitionOpts = Lib.extendFlat({}, transitionOpts); traceTransitionOpts.duration = 0; // This means do not transition cartesian traces, // this happens on layout-only (e.g. axis range) animations delete transitionedTraces.cartesian; } else { traceTransitionOpts = transitionOpts; } // Note that we pass a callback to *create* the callback that must be invoked on completion. // This is since not all traces know about transitions, so it greatly simplifies matters if // the trace is responsible for creating a callback, if needed, and then executing it when // the time is right. for(var n in transitionedTraces) { var traceIndices = transitionedTraces[n]; var _module = gd._fullData[traceIndices[0]]._module; _module.basePlotModule.plot(gd, traceIndices, traceTransitionOpts, makeCallback); } }; return _transition(gd, transitionOpts, opts); }; /** * Transition to a set of new data and layout properties from Plotly.react * * @param {DOM element} gd * @param {object} restyleFlags * - anim {'all'|'some'} * @param {object} relayoutFlags * - anim {'all'|'some'} * @param {object} oldFullLayout : old (pre Plotly.react) fullLayout */ plots.transitionFromReact = function(gd, restyleFlags, relayoutFlags, oldFullLayout) { var fullLayout = gd._fullLayout; var transitionOpts = fullLayout.transition; var opts = {}; var axEdits = []; opts.prepareFn = function() { var subplots = fullLayout._plots; // no need to redraw at end of transition, // if all changes are animatable opts.redraw = false; if(restyleFlags.anim === 'some') opts.redraw = true; if(relayoutFlags.anim === 'some') opts.redraw = true; for(var k in subplots) { var plotinfo = subplots[k]; var xa = plotinfo.xaxis; var ya = plotinfo.yaxis; var xr0 = oldFullLayout[xa._name].range.slice(); var yr0 = oldFullLayout[ya._name].range.slice(); var xr1 = xa.range.slice(); var yr1 = ya.range.slice(); xa.setScale(); ya.setScale(); var editX = null; var editY = null; if(xa.r2l(xr0[0]) !== xa.r2l(xr1[0]) || xa.r2l(xr0[1]) !== xa.r2l(xr1[1])) { editX = {xr0: xr0, xr1: xr1}; } if(ya.r2l(yr0[0]) !== ya.r2l(yr1[0]) || ya.r2l(yr0[1]) !== ya.r2l(yr1[1])) { editY = {yr0: yr0, yr1: yr1}; } if(editX || editY) { axEdits.push(Lib.extendFlat({plotinfo: plotinfo}, editX, editY)); } } return Promise.resolve(); }; opts.runFn = function(makeCallback) { var fullData = gd._fullData; var fullLayout = gd._fullLayout; var basePlotModules = fullLayout._basePlotModules; var axisTransitionOpts; var traceTransitionOpts; var transitionedTraces; var allTraceIndices = []; for(var i = 0; i < fullData.length; i++) { allTraceIndices.push(i); } function transitionAxes() { if(!gd._fullLayout) return; for(var j = 0; j < basePlotModules.length; j++) { if(basePlotModules[j].transitionAxes) { basePlotModules[j].transitionAxes(gd, axEdits, axisTransitionOpts, makeCallback); } } } function transitionTraces() { if(!gd._fullLayout) return; for(var j = 0; j < basePlotModules.length; j++) { basePlotModules[j].plot(gd, transitionedTraces, traceTransitionOpts, makeCallback); } } if(axEdits.length && restyleFlags.anim) { if(transitionOpts.ordering === 'traces first') { axisTransitionOpts = Lib.extendFlat({}, transitionOpts, {duration: 0}); transitionedTraces = allTraceIndices; traceTransitionOpts = transitionOpts; setTimeout(transitionAxes, transitionOpts.duration); transitionTraces(); } else { axisTransitionOpts = transitionOpts; transitionedTraces = null; traceTransitionOpts = Lib.extendFlat({}, transitionOpts, {duration: 0}); setTimeout(transitionTraces, axisTransitionOpts.duration); transitionAxes(); } } else if(axEdits.length) { axisTransitionOpts = transitionOpts; transitionAxes(); } else if(restyleFlags.anim) { transitionedTraces = allTraceIndices; traceTransitionOpts = transitionOpts; transitionTraces(); } }; return _transition(gd, transitionOpts, opts); }; /** * trace/layout transition wrapper that works * for transitions initiated by Plotly.animate and Plotly.react. * * @param {DOM element} gd * @param {object} transitionOpts * @param {object} opts * - redraw {boolean} * - prepareFn {function} *should return a Promise* * - runFn {function} ran inside executeTransitions */ function _transition(gd, transitionOpts, opts) { var aborted = false; function executeCallbacks(list) { var p = Promise.resolve(); if(!list) return p; while(list.length) { p = p.then((list.shift())); } return p; } function flushCallbacks(list) { if(!list) return; while(list.length) { list.shift(); } } function executeTransitions() { gd.emit('plotly_transitioning', []); return new Promise(function(resolve) { // This flag is used to disabled things like autorange: gd._transitioning = true; // When instantaneous updates are coming through quickly, it's too much to simply disable // all interaction, so store this flag so we can disambiguate whether mouse interactions // should be fully disabled or not: if(transitionOpts.duration > 0) { gd._transitioningWithDuration = true; } // If another transition is triggered, this callback will be executed simply because it's // in the interruptCallbacks queue. If this transition completes, it will instead flush // that queue and forget about this callback. gd._transitionData._interruptCallbacks.push(function() { aborted = true; }); if(opts.redraw) { gd._transitionData._interruptCallbacks.push(function() { return Registry.call('redraw', gd); }); } // Emit this and make sure it happens last: gd._transitionData._interruptCallbacks.push(function() { gd.emit('plotly_transitioninterrupted', []); }); // Construct callbacks that are executed on transition end. This ensures the d3 transitions // are *complete* before anything else is done. var numCallbacks = 0; var numCompleted = 0; function makeCallback() { numCallbacks++; return function() { numCompleted++; // When all are complete, perform a redraw: if(!aborted && numCompleted === numCallbacks) { completeTransition(resolve); } }; } opts.runFn(makeCallback); // If nothing else creates a callback, then this will trigger the completion in the next tick: setTimeout(makeCallback()); }); } function completeTransition(callback) { // This a simple workaround for tests which purge the graph before animations // have completed. That's not a very common case, so this is the simplest // fix. if(!gd._transitionData) return; flushCallbacks(gd._transitionData._interruptCallbacks); return Promise.resolve().then(function() { if(opts.redraw) { return Registry.call('redraw', gd); } }).then(function() { // Set transitioning false again once the redraw has occurred. This is used, for example, // to prevent the trailing redraw from autoranging: gd._transitioning = false; gd._transitioningWithDuration = false; gd.emit('plotly_transitioned', []); }).then(callback); } function interruptPreviousTransitions() { // Fail-safe against purged plot: if(!gd._transitionData) return; // If a transition is interrupted, set this to false. At the moment, the only thing that would // interrupt a transition is another transition, so that it will momentarily be set to true // again, but this determines whether autorange or dragbox work, so it's for the sake of // cleanliness: gd._transitioning = false; return executeCallbacks(gd._transitionData._interruptCallbacks); } var seq = [ plots.previousPromises, interruptPreviousTransitions, opts.prepareFn, plots.rehover, plots.reselect, executeTransitions ]; var transitionStarting = Lib.syncOrAsync(seq, gd); if(!transitionStarting || !transitionStarting.then) { transitionStarting = Promise.resolve(); } return transitionStarting.then(function() { return gd; }); } plots.doCalcdata = function(gd, traces) { var axList = axisIDs.list(gd); var fullData = gd._fullData; var fullLayout = gd._fullLayout; var trace, _module, i, j; // XXX: Is this correct? Needs a closer look so that *some* traces can be recomputed without // *all* needing doCalcdata: var calcdata = new Array(fullData.length); var oldCalcdata = (gd.calcdata || []).slice(); gd.calcdata = calcdata; // extra helper variables // how many box/violins plots do we have (in case they're grouped) fullLayout._numBoxes = 0; fullLayout._numViolins = 0; // initialize violin per-scale-group stats container fullLayout._violinScaleGroupStats = {}; // for calculating avg luminosity of heatmaps gd._hmpixcount = 0; gd._hmlumcount = 0; // for sharing colors across pies / sunbursts / treemap / icicle / funnelarea (and for legend) fullLayout._piecolormap = {}; fullLayout._sunburstcolormap = {}; fullLayout._treemapcolormap = {}; fullLayout._iciclecolormap = {}; fullLayout._funnelareacolormap = {}; // If traces were specified and this trace was not included, // then transfer it over from the old calcdata: for(i = 0; i < fullData.length; i++) { if(Array.isArray(traces) && traces.indexOf(i) === -1) { calcdata[i] = oldCalcdata[i]; continue; } } for(i = 0; i < fullData.length; i++) { trace = fullData[i]; trace._arrayAttrs = PlotSchema.findArrayAttributes(trace); // keep track of trace extremes (for autorange) in here trace._extremes = {}; } // add polar axes to axis list var polarIds = fullLayout._subplots.polar || []; for(i = 0; i < polarIds.length; i++) { axList.push( fullLayout[polarIds[i]].radialaxis, fullLayout[polarIds[i]].angularaxis ); } // clear relinked cmin/cmax values in shared axes to start aggregation from scratch for(var k in fullLayout._colorAxes) { var cOpts = fullLayout[k]; if(cOpts.cauto !== false) { delete cOpts.cmin; delete cOpts.cmax; } } var hasCalcTransform = false; function transformCalci(i) { trace = fullData[i]; _module = trace._module; if(trace.visible === true && trace.transforms) { // we need one round of trace module calc before // the calc transform to 'fill in' the categories list // used for example in the data-to-coordinate method if(_module && _module.calc) { var cdi = _module.calc(gd, trace); // must clear scene 'batches', so that 2nd // _module.calc call starts from scratch if(cdi[0] && cdi[0].t && cdi[0].t._scene) { delete cdi[0].t._scene.dirty; } } for(j = 0; j < trace.transforms.length; j++) { var transform = trace.transforms[j]; _module = transformsRegistry[transform.type]; if(_module && _module.calcTransform) { trace._hasCalcTransform = true; hasCalcTransform = true; _module.calcTransform(gd, trace, transform); } } } } function calci(i, isContainer) { trace = fullData[i]; _module = trace._module; if(!!_module.isContainer !== isContainer) return; var cd = []; if(trace.visible === true && trace._length !== 0) { // clear existing ref in case it got relinked delete trace._indexToPoints; // keep ref of index-to-points map object of the *last* enabled transform, // this index-to-points map object is required to determine the calcdata indices // that correspond to input indices (e.g. from 'selectedpoints') var transforms = trace.transforms || []; for(j = transforms.length - 1; j >= 0; j--) { if(transforms[j].enabled) { trace._indexToPoints = transforms[j]._indexToPoints; break; } } if(_module && _module.calc) { cd = _module.calc(gd, trace); } } // Make sure there is a first point. // // This ensures there is a calcdata item for every trace, // even if cartesian logic doesn't handle it (for things like legends). if(!Array.isArray(cd) || !cd[0]) { cd = [{x: BADNUM, y: BADNUM}]; } // add the trace-wide properties to the first point, // per point properties to every point // t is the holder for trace-wide properties if(!cd[0].t) cd[0].t = {}; cd[0].trace = trace; calcdata[i] = cd; } setupAxisCategories(axList, fullData, fullLayout); // 'transform' loop - must calc container traces first // so that if their dependent traces can get transform properly for(i = 0; i < fullData.length; i++) calci(i, true); for(i = 0; i < fullData.length; i++) transformCalci(i); // clear stuff that should recomputed in 'regular' loop if(hasCalcTransform) setupAxisCategories(axList, fullData, fullLayout); // 'regular' loop - make sure container traces (eg carpet) calc before // contained traces (eg contourcarpet) for(i = 0; i < fullData.length; i++) calci(i, true); for(i = 0; i < fullData.length; i++) calci(i, false); doCrossTraceCalc(gd); // Sort axis categories per value if specified var sorted = sortAxisCategoriesByValue(axList, gd); if(sorted.length) { // how many box/violins plots do we have (in case they're grouped) fullLayout._numBoxes = 0; fullLayout._numViolins = 0; // If a sort operation was performed, run calc() again for(i = 0; i < sorted.length; i++) calci(sorted[i], true); for(i = 0; i < sorted.length; i++) calci(sorted[i], false); doCrossTraceCalc(gd); } Registry.getComponentMethod('fx', 'calc')(gd); Registry.getComponentMethod('errorbars', 'calc')(gd); }; var sortAxisCategoriesByValueRegex = /(total|sum|min|max|mean|geometric mean|median) (ascending|descending)/; function sortAxisCategoriesByValue(axList, gd) { var affectedTraces = []; var i, j, k, l, o; function zMapCategory(type, ax, value) { var axLetter = ax._id.charAt(0); if(type === 'histogram2dcontour') { var counterAxLetter = ax._counterAxes[0]; var counterAx = axisIDs.getFromId(gd, counterAxLetter); var xCategorical = axLetter === 'x' || (counterAxLetter === 'x' && counterAx.type === 'category'); var yCategorical = axLetter === 'y' || (counterAxLetter === 'y' && counterAx.type === 'category'); return function(o, l) { if(o === 0 || l === 0) return -1; // Skip first row and column if(xCategorical && o === value[l].length - 1) return -1; if(yCategorical && l === value.length - 1) return -1; return (axLetter === 'y' ? l : o) - 1; }; } else { return function(o, l) { return axLetter === 'y' ? l : o; }; } } var aggFn = { min: function(values) {return Lib.aggNums(Math.min, null, values);}, max: function(values) {return Lib.aggNums(Math.max, null, values);}, sum: function(values) {return Lib.aggNums(function(a, b) { return a + b;}, null, values);}, total: function(values) {return Lib.aggNums(function(a, b) { return a + b;}, null, values);}, mean: function(values) {return Lib.mean(values);}, 'geometric mean': function(values) {return Lib.geometricMean(values);}, median: function(values) {return Lib.median(values);} }; function sortAscending(a, b) { return a[1] - b[1]; } function sortDescending(a, b) { return b[1] - a[1]; } for(i = 0; i < axList.length; i++) { var ax = axList[i]; if(ax.type !== 'category') continue; // Order by value var match = ax.categoryorder.match(sortAxisCategoriesByValueRegex); if(match) { var aggregator = match[1]; var order = match[2]; var axLetter = ax._id.charAt(0); var isX = axLetter === 'x'; // Store values associated with each category var categoriesValue = []; for(j = 0; j < ax._categories.length; j++) { categoriesValue.push([ax._categories[j], []]); } // Collect values across traces for(j = 0; j < ax._traceIndices.length; j++) { var traceIndex = ax._traceIndices[j]; var fullTrace = gd._fullData[traceIndex]; // Skip over invisible traces if(fullTrace.visible !== true) continue; var type = fullTrace.type; if(Registry.traceIs(fullTrace, 'histogram')) { delete fullTrace._xautoBinFinished; delete fullTrace._yautoBinFinished; } var isSplom = type === 'splom'; var isScattergl = type === 'scattergl'; var cd = gd.calcdata[traceIndex]; for(k = 0; k < cd.length; k++) { var cdi = cd[k]; var catIndex, value; if(isSplom) { // If `splom`, collect values across dimensions // Find which dimension the current axis is representing var currentDimensionIndex = fullTrace._axesDim[ax._id]; // Apply logic to associated x axis if it's defined if(!isX) { var associatedXAxisID = fullTrace._diag[currentDimensionIndex][0]; if(associatedXAxisID) ax = gd._fullLayout[axisIDs.id2name(associatedXAxisID)]; } var categories = cdi.trace.dimensions[currentDimensionIndex].values; for(l = 0; l < categories.length; l++) { catIndex = ax._categoriesMap[categories[l]]; // Collect associated values at index `l` over all other dimensions for(o = 0; o < cdi.trace.dimensions.length; o++) { if(o === currentDimensionIndex) continue; var dimension = cdi.trace.dimensions[o]; categoriesValue[catIndex][1].push(dimension.values[l]); } } } else if(isScattergl) { // If `scattergl`, collect all values stashed under cdi.t for(l = 0; l < cdi.t.x.length; l++) { if(isX) { catIndex = cdi.t.x[l]; value = cdi.t.y[l]; } else { catIndex = cdi.t.y[l]; value = cdi.t.x[l]; } categoriesValue[catIndex][1].push(value); } // must clear scene 'batches', so that 2nd // _module.calc call starts from scratch if(cdi.t && cdi.t._scene) { delete cdi.t._scene.dirty; } } else if(cdi.hasOwnProperty('z')) { // If 2dMap, collect values in `z` value = cdi.z; var mapping = zMapCategory(fullTrace.type, ax, value); for(l = 0; l < value.length; l++) { for(o = 0; o < value[l].length; o++) { catIndex = mapping(o, l); if(catIndex + 1) categoriesValue[catIndex][1].push(value[l][o]); } } } else { // For all other 2d cartesian traces catIndex = cdi.p; if(catIndex === undefined) catIndex = cdi[axLetter]; value = cdi.s; if(value === undefined) value = cdi.v; if(value === undefined) value = isX ? cdi.y : cdi.x; if(!Array.isArray(value)) { if(value === undefined) value = []; else value = [value]; } for(l = 0; l < value.length; l++) { categoriesValue[catIndex][1].push(value[l]); } } } } ax._categoriesValue = categoriesValue; var categoriesAggregatedValue = []; for(j = 0; j < categoriesValue.length; j++) { categoriesAggregatedValue.push([ categoriesValue[j][0], aggFn[aggregator](categoriesValue[j][1]) ]); } // Sort by aggregated value categoriesAggregatedValue.sort(order === 'descending' ? sortDescending : sortAscending); ax._categoriesAggregatedValue = categoriesAggregatedValue; // Set new category order ax._initialCategories = categoriesAggregatedValue.map(function(c) { return c[0]; }); // Sort all matching axes affectedTraces = affectedTraces.concat(ax.sortByInitialCategories()); } } return affectedTraces; } function setupAxisCategories(axList, fullData, fullLayout) { var axLookup = {}; function setupOne(ax) { ax.clearCalc(); if(ax.type === 'multicategory') { ax.setupMultiCategory(fullData); } axLookup[ax._id] = 1; } Lib.simpleMap(axList, setupOne); // look into match groups for 'missing' axes var matchGroups = fullLayout._axisMatchGroups || []; for(var i = 0; i < matchGroups.length; i++) { for(var axId in matchGroups[i]) { if(!axLookup[axId]) { setupOne(fullLayout[axisIDs.id2name(axId)]); } } } } function doCrossTraceCalc(gd) { var fullLayout = gd._fullLayout; var modules = fullLayout._visibleModules; var hash = {}; var i, j, k; // position and range calculations for traces that // depend on each other ie bars (stacked or grouped) // and boxes (grouped) push each other out of the way for(j = 0; j < modules.length; j++) { var _module = modules[j]; var fn = _module.crossTraceCalc; if(fn) { var spType = _module.basePlotModule.name; if(hash[spType]) { Lib.pushUnique(hash[spType], fn); } else { hash[spType] = [fn]; } } } for(k in hash) { var methods = hash[k]; var subplots = fullLayout._subplots[k]; if(Array.isArray(subplots)) { for(i = 0; i < subplots.length; i++) { var sp = subplots[i]; var spInfo = k === 'cartesian' ? fullLayout._plots[sp] : fullLayout[sp]; for(j = 0; j < methods.length; j++) { methods[j](gd, spInfo, sp); } } } else { for(j = 0; j < methods.length; j++) { methods[j](gd); } } } } plots.rehover = function(gd) { if(gd._fullLayout._rehover) { gd._fullLayout._rehover(); } }; plots.redrag = function(gd) { if(gd._fullLayout._redrag) { gd._fullLayout._redrag(); } }; plots.reselect = function(gd) { var fullLayout = gd._fullLayout; var A = (gd.layout || {}).selections; var B = fullLayout._previousSelections; fullLayout._previousSelections = A; var mayEmitSelected = fullLayout._reselect || JSON.stringify(A) !== JSON.stringify(B); Registry.getComponentMethod('selections', 'reselect')(gd, mayEmitSelected); }; plots.generalUpdatePerTraceModule = function(gd, subplot, subplotCalcData, subplotLayout) { var traceHashOld = subplot.traceHash; var traceHash = {}; var i; // build up moduleName -> calcData hash for(i = 0; i < subplotCalcData.length; i++) { var calcTraces = subplotCalcData[i]; var trace = calcTraces[0].trace; // skip over visible === false traces // as they don't have `_module` ref if(trace.visible) { traceHash[trace.type] = traceHash[trace.type] || []; traceHash[trace.type].push(calcTraces); } } // when a trace gets deleted, make sure that its module's // plot method is called so that it is properly // removed from the DOM. for(var moduleNameOld in traceHashOld) { if(!traceHash[moduleNameOld]) { var fakeCalcTrace = traceHashOld[moduleNameOld][0]; var fakeTrace = fakeCalcTrace[0].trace; fakeTrace.visible = false; traceHash[moduleNameOld] = [fakeCalcTrace]; } } // call module plot method for(var moduleName in traceHash) { var moduleCalcData = traceHash[moduleName]; var _module = moduleCalcData[0][0].trace._module; _module.plot(gd, subplot, Lib.filterVisible(moduleCalcData), subplotLayout); } // update moduleName -> calcData hash subplot.traceHash = traceHash; }; plots.plotBasePlot = function(desiredType, gd, traces, transitionOpts, makeOnCompleteCallback) { var _module = Registry.getModule(desiredType); var cdmodule = getModuleCalcData(gd.calcdata, _module)[0]; _module.plot(gd, cdmodule, transitionOpts, makeOnCompleteCallback); }; plots.cleanBasePlot = function(desiredType, newFullData, newFullLayout, oldFullData, oldFullLayout) { var had = (oldFullLayout._has && oldFullLayout._has(desiredType)); var has = (newFullLayout._has && newFullLayout._has(desiredType)); if(had && !has) { oldFullLayout['_' + desiredType + 'layer'].selectAll('g.trace').remove(); } };