plotly.js
Version:
The open source javascript graphing library that powers plotly
1,486 lines (1,217 loc) • 116 kB
JavaScript
'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 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 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.
*
* 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
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 hadCartesian = !!(oldFullLayout._has && oldFullLayout._has('cartesian'));
var hasCartesian = !!(newFullLayout._has && newFullLayout._has('cartesian'));
var hadBgLayer = hadCartesian;
var hasBgLayer = hasCartesian;
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 || [];
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._fullInput = 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(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);
}
}
}
return traceOut;
};
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(co