plotly.js
Version:
The open source javascript graphing library that powers plotly
1,423 lines (1,170 loc) • 120 kB
JavaScript
/**
* 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