UNPKG

plotly.js

Version:

The open source javascript graphing library that powers plotly

1,423 lines (1,170 loc) 120 kB
/** * Copyright 2012-2020, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ 'use strict'; var d3 = require('d3'); var timeFormatLocale = require('d3-time-format').timeFormatLocale; var isNumeric = require('fast-isnumeric'); 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 clearSelect = require('./cartesian/handle_outline').clearSelect; 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'); // TODO make this a plot attribute? plots.fontWeight = 'normal'; 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); var fullLayout = gd._fullLayout || {}; var hasPolar = fullLayout._has && fullLayout._has('polar'); var hasLegacyPolar = !hasPolar && gd.data && gd.data[0] && gd.data[0].r; // do not work if polar is present if(hasLegacyPolar) return; return new Promise(function(resolve) { setTimeout(function() { 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 Plotly.plot() 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'), 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 ); // TODO remove in v2.0.0 // add has-plot-type refs to fullLayout for backward compatibility newFullLayout._hasCartesian = newFullLayout._has('cartesian'); newFullLayout._hasGeo = newFullLayout._has('geo'); newFullLayout._hasGL3D = newFullLayout._has('gl3d'); newFullLayout._hasGL2D = newFullLayout._has('gl2d'); newFullLayout._hasTernary = newFullLayout._has('ternary'); newFullLayout._hasPie = newFullLayout._has('pie'); // 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) { clearSelect({ // 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 v2, 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: d3.locale(formatObj).numberFormat, 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('legendgroup'); 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) { coerce('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