UNPKG

plotly.js

Version:

The open source javascript graphing library that powers plotly

1,488 lines (1,261 loc) 144 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 isNumeric = require('fast-isnumeric'); var hasHover = require('has-hover'); var Lib = require('../lib'); var nestedProperty = Lib.nestedProperty; var Events = require('../lib/events'); var Queue = require('../lib/queue'); var Registry = require('../registry'); var PlotSchema = require('./plot_schema'); var Plots = require('../plots/plots'); var Polar = require('../plots/polar/legacy'); var Axes = require('../plots/cartesian/axes'); var Drawing = require('../components/drawing'); var Color = require('../components/color'); var initInteractions = require('../plots/cartesian/graph_interact').initInteractions; var xmlnsNamespaces = require('../constants/xmlns_namespaces'); var svgTextUtils = require('../lib/svg_text_utils'); var clearSelect = require('../plots/cartesian/select').clearSelect; var dfltConfig = require('./plot_config').dfltConfig; var manageArrays = require('./manage_arrays'); var helpers = require('./helpers'); var subroutines = require('./subroutines'); var editTypes = require('./edit_types'); var AX_NAME_PATTERN = require('../plots/cartesian/constants').AX_NAME_PATTERN; var numericNameWarningCount = 0; var numericNameWarningCountLimit = 5; /** * Main plot-creation function * * @param {string id or DOM element} gd * the id or DOM element of the graph container div * @param {array of objects} data * array of traces, containing the data and display information for each trace * @param {object} layout * object describing the overall display of the plot, * all the stuff that doesn't pertain to any individual trace * @param {object} config * configuration options (see ./plot_config.js for more info) * * OR * * @param {string id or DOM element} gd * the id or DOM element of the graph container div * @param {object} figure * object containing `data`, `layout`, `config`, and `frames` members * */ function plot(gd, data, layout, config) { var frames; gd = Lib.getGraphDiv(gd); // Events.init is idempotent and bails early if gd has already been init'd Events.init(gd); if(Lib.isPlainObject(data)) { var obj = data; data = obj.data; layout = obj.layout; config = obj.config; frames = obj.frames; } var okToPlot = Events.triggerHandler(gd, 'plotly_beforeplot', [data, layout, config]); if(okToPlot === false) return Promise.reject(); // if there's no data or layout, and this isn't yet a plotly plot // container, log a warning to help plotly.js users debug if(!data && !layout && !Lib.isPlotDiv(gd)) { Lib.warn('Calling Plotly.plot as if redrawing ' + 'but this container doesn\'t yet have a plot.', gd); } function addFrames() { if(frames) { return exports.addFrames(gd, frames); } } // transfer configuration options to gd until we move over to // a more OO like model setPlotContext(gd, config); if(!layout) layout = {}; // hook class for plots main container (in case of plotly.js // this won't be #embedded-graph or .js-tab-contents) d3.select(gd).classed('js-plotly-plot', true); // off-screen getBoundingClientRect testing space, // in #js-plotly-tester (and stored as Drawing.tester) // so we can share cached text across tabs Drawing.makeTester(); // collect promises for any async actions during plotting // any part of the plotting code can push to gd._promises, then // before we move to the next step, we check that they're all // complete, and empty out the promise list again. if(!Array.isArray(gd._promises)) gd._promises = []; var graphWasEmpty = ((gd.data || []).length === 0 && Array.isArray(data)); // if there is already data on the graph, append the new data // if you only want to redraw, pass a non-array for data if(Array.isArray(data)) { helpers.cleanData(data); if(graphWasEmpty) gd.data = data; else gd.data.push.apply(gd.data, data); // for routines outside graph_obj that want a clean tab // (rather than appending to an existing one) gd.empty // is used to determine whether to make a new tab gd.empty = false; } if(!gd.layout || graphWasEmpty) { gd.layout = helpers.cleanLayout(layout); } Plots.supplyDefaults(gd); var fullLayout = gd._fullLayout; var hasCartesian = fullLayout._has('cartesian'); // Legacy polar plots if(!fullLayout._has('polar') && data && data[0] && data[0].r) { Lib.log('Legacy polar charts are deprecated!'); return plotLegacyPolar(gd, data, layout); } // so we don't try to re-call Plotly.plot from inside // legend and colorbar, if margins changed fullLayout._replotting = true; // make or remake the framework if we need to if(graphWasEmpty || fullLayout._shouldCreateBgLayer) { makePlotFramework(gd); if(fullLayout._shouldCreateBgLayer) { delete fullLayout._shouldCreateBgLayer; } } // polar need a different framework if(gd.framework !== makePlotFramework) { gd.framework = makePlotFramework; makePlotFramework(gd); } // clear gradient defs on each .plot call, because we know we'll loop through all traces Drawing.initGradients(gd); // save initial show spikes once per graph if(graphWasEmpty) Axes.saveShowSpikeInitial(gd); // prepare the data and find the autorange // generate calcdata, if we need to // to force redoing calcdata, just delete it before calling Plotly.plot var recalc = !gd.calcdata || gd.calcdata.length !== (gd._fullData || []).length; if(recalc) Plots.doCalcdata(gd); // in case it has changed, attach fullData traces to calcdata for(var i = 0; i < gd.calcdata.length; i++) { gd.calcdata[i][0].trace = gd._fullData[i]; } // make the figure responsive if(gd._context.responsive) { if(!gd._responsiveChartHandler) { // Keep a reference to the resize handler to purge it down the road gd._responsiveChartHandler = function() { if(!Lib.isHidden(gd)) Plots.resize(gd); }; // Listen to window resize window.addEventListener('resize', gd._responsiveChartHandler); } } else { Lib.clearResponsive(gd); } /* * start async-friendly code - now we're actually drawing things */ var oldMargins = Lib.extendFlat({}, fullLayout._size); // draw framework first so that margin-pushing // components can position themselves correctly var drawFrameworkCalls = 0; function drawFramework() { var basePlotModules = fullLayout._basePlotModules; for(var i = 0; i < basePlotModules.length; i++) { if(basePlotModules[i].drawFramework) { basePlotModules[i].drawFramework(gd); } } if(!fullLayout._glcanvas && fullLayout._has('gl')) { fullLayout._glcanvas = fullLayout._glcontainer.selectAll('.gl-canvas').data([{ key: 'contextLayer', context: true, pick: false }, { key: 'focusLayer', context: false, pick: false }, { key: 'pickLayer', context: false, pick: true }], function(d) { return d.key; }); fullLayout._glcanvas.enter().append('canvas') .attr('class', function(d) { return 'gl-canvas gl-canvas-' + d.key.replace('Layer', ''); }) .style({ position: 'absolute', top: 0, left: 0, overflow: 'visible', 'pointer-events': 'none' }); } if(fullLayout._glcanvas) { fullLayout._glcanvas .attr('width', fullLayout.width) .attr('height', fullLayout.height); var regl = fullLayout._glcanvas.data()[0].regl; if(regl) { // Unfortunately, this can happen when relayouting to large // width/height on some browsers. if(Math.floor(fullLayout.width) !== regl._gl.drawingBufferWidth || Math.floor(fullLayout.height) !== regl._gl.drawingBufferHeight ) { var msg = 'WebGL context buffer and canvas dimensions do not match due to browser/WebGL bug.'; if(drawFrameworkCalls) { Lib.error(msg); } else { Lib.log(msg + ' Clearing graph and plotting again.'); Plots.cleanPlot([], {}, gd._fullData, fullLayout); Plots.supplyDefaults(gd); fullLayout = gd._fullLayout; Plots.doCalcdata(gd); drawFrameworkCalls++; return drawFramework(); } } } } if(fullLayout.modebar.orientation === 'h') { fullLayout._modebardiv .style('height', null) .style('width', '100%'); } else { fullLayout._modebardiv .style('width', null) .style('height', fullLayout.height + 'px'); } return Plots.previousPromises(gd); } // draw anything that can affect margins. function marginPushers() { // First reset the list of things that are allowed to change the margins // So any deleted traces or components will be wiped out of the // automargin calculation. // This means *every* margin pusher must be listed here, even if it // doesn't actually try to push the margins until later. Plots.clearAutoMarginIds(gd); subroutines.drawMarginPushers(gd); Axes.allowAutoMargin(gd); // TODO can this be moved elsewhere? if(fullLayout._has('pie')) { var fullData = gd._fullData; for(var i = 0; i < fullData.length; i++) { var trace = fullData[i]; if(trace.type === 'pie' && trace.automargin) { Plots.allowAutoMargin(gd, 'pie.' + trace.uid + '.automargin'); } } } Plots.doAutoMargin(gd); return Plots.previousPromises(gd); } // in case the margins changed, draw margin pushers again function marginPushersAgain() { if(!Plots.didMarginChange(oldMargins, fullLayout._size)) return; return Lib.syncOrAsync([ marginPushers, subroutines.layoutStyles ], gd); } function positionAndAutorange() { if(!recalc) { doAutoRangeAndConstraints(); return; } // TODO: autosize extra for text markers and images // see https://github.com/plotly/plotly.js/issues/1111 return Lib.syncOrAsync([ Registry.getComponentMethod('shapes', 'calcAutorange'), Registry.getComponentMethod('annotations', 'calcAutorange'), doAutoRangeAndConstraints ], gd); } function doAutoRangeAndConstraints() { if(gd._transitioning) return; subroutines.doAutoRangeAndConstraints(gd); // store initial ranges *after* enforcing constraints, otherwise // we will never look like we're at the initial ranges if(graphWasEmpty) Axes.saveRangeInitial(gd); // this one is different from shapes/annotations calcAutorange // the others incorporate those components into ax._extremes, // this one actually sets the ranges in rangesliders. Registry.getComponentMethod('rangeslider', 'calcAutorange')(gd); } // draw ticks, titles, and calculate axis scaling (._b, ._m) function drawAxes() { return Axes.draw(gd, graphWasEmpty ? '' : 'redraw'); } var seq = [ Plots.previousPromises, addFrames, drawFramework, marginPushers, marginPushersAgain ]; if(hasCartesian) seq.push(positionAndAutorange); seq.push(subroutines.layoutStyles); if(hasCartesian) { seq.push( drawAxes, function insideTickLabelsAutorange(gd) { if(gd._fullLayout._insideTickLabelsAutorange) { relayout(gd, gd._fullLayout._insideTickLabelsAutorange).then(function() { gd._fullLayout._insideTickLabelsAutorange = undefined; }); } } ); } seq.push( subroutines.drawData, subroutines.finalDraw, initInteractions, Plots.addLinks, Plots.rehover, Plots.redrag, // TODO: doAutoMargin is only needed here for axis automargin, which // happens outside of marginPushers where all the other automargins are // calculated. Would be much better to separate margin calculations from // component drawing - see https://github.com/plotly/plotly.js/issues/2704 Plots.doAutoMargin, saveRangeInitialForInsideTickLabels, Plots.previousPromises ); function saveRangeInitialForInsideTickLabels(gd) { if(gd._fullLayout._insideTickLabelsAutorange) { if(graphWasEmpty) Axes.saveRangeInitial(gd, true); } } // even if everything we did was synchronous, return a promise // so that the caller doesn't care which route we took var plotDone = Lib.syncOrAsync(seq, gd); if(!plotDone || !plotDone.then) plotDone = Promise.resolve(); return plotDone.then(function() { emitAfterPlot(gd); return gd; }); } function emitAfterPlot(gd) { var fullLayout = gd._fullLayout; if(fullLayout._redrawFromAutoMarginCount) { fullLayout._redrawFromAutoMarginCount--; } else { gd.emit('plotly_afterplot'); } } function setPlotConfig(obj) { return Lib.extendFlat(dfltConfig, obj); } function setBackground(gd, bgColor) { try { gd._fullLayout._paper.style('background', bgColor); } catch(e) { Lib.error(e); } } function opaqueSetBackground(gd, bgColor) { var blend = Color.combine(bgColor, 'white'); setBackground(gd, blend); } function setPlotContext(gd, config) { if(!gd._context) { gd._context = Lib.extendDeep({}, dfltConfig); // stash <base> href, used to make robust clipPath URLs var base = d3.select('base'); gd._context._baseUrl = base.size() && base.attr('href') ? window.location.href.split('#')[0] : ''; } var context = gd._context; var i, keys, key; if(config) { keys = Object.keys(config); for(i = 0; i < keys.length; i++) { key = keys[i]; if(key === 'editable' || key === 'edits') continue; if(key in context) { if(key === 'setBackground' && config[key] === 'opaque') { context[key] = opaqueSetBackground; } else { context[key] = config[key]; } } } // map plot3dPixelRatio to plotGlPixelRatio for backward compatibility if(config.plot3dPixelRatio && !context.plotGlPixelRatio) { context.plotGlPixelRatio = context.plot3dPixelRatio; } // now deal with editable and edits - first editable overrides // everything, then edits refines var editable = config.editable; if(editable !== undefined) { // we're not going to *use* context.editable, we're only going to // use context.edits... but keep it for the record context.editable = editable; keys = Object.keys(context.edits); for(i = 0; i < keys.length; i++) { context.edits[keys[i]] = editable; } } if(config.edits) { keys = Object.keys(config.edits); for(i = 0; i < keys.length; i++) { key = keys[i]; if(key in context.edits) { context.edits[key] = config.edits[key]; } } } // not part of the user-facing config options context._exportedPlot = config._exportedPlot; } // staticPlot forces a bunch of others: if(context.staticPlot) { context.editable = false; context.edits = {}; context.autosizable = false; context.scrollZoom = false; context.doubleClick = false; context.showTips = false; context.showLink = false; context.displayModeBar = false; } // make sure hover-only devices have mode bar visible if(context.displayModeBar === 'hover' && !hasHover) { context.displayModeBar = true; } // default and fallback for setBackground if(context.setBackground === 'transparent' || typeof context.setBackground !== 'function') { context.setBackground = setBackground; } // Check if gd has a specified widht/height to begin with context._hasZeroHeight = context._hasZeroHeight || gd.clientHeight === 0; context._hasZeroWidth = context._hasZeroWidth || gd.clientWidth === 0; // fill context._scrollZoom helper to help manage scrollZoom flaglist var szIn = context.scrollZoom; var szOut = context._scrollZoom = {}; if(szIn === true) { szOut.cartesian = 1; szOut.gl3d = 1; szOut.geo = 1; szOut.mapbox = 1; } else if(typeof szIn === 'string') { var parts = szIn.split('+'); for(i = 0; i < parts.length; i++) { szOut[parts[i]] = 1; } } else if(szIn !== false) { szOut.gl3d = 1; szOut.geo = 1; szOut.mapbox = 1; } } function plotLegacyPolar(gd, data, layout) { // build or reuse the container skeleton var plotContainer = d3.select(gd).selectAll('.plot-container') .data([0]); plotContainer.enter() .insert('div', ':first-child') .classed('plot-container plotly', true); var paperDiv = plotContainer.selectAll('.svg-container') .data([0]); paperDiv.enter().append('div') .classed('svg-container', true) .style('position', 'relative'); // empty it everytime for now paperDiv.html(''); // fulfill gd requirements if(data) gd.data = data; if(layout) gd.layout = layout; Polar.manager.fillLayout(gd); // resize canvas paperDiv.style({ width: gd._fullLayout.width + 'px', height: gd._fullLayout.height + 'px' }); // instantiate framework gd.framework = Polar.manager.framework(gd); // plot gd.framework({data: gd.data, layout: gd.layout}, paperDiv.node()); // set undo point gd.framework.setUndoPoint(); // get the resulting svg for extending it var polarPlotSVG = gd.framework.svg(); // editable title var opacity = 1; var txt = gd._fullLayout.title ? gd._fullLayout.title.text : ''; if(txt === '' || !txt) opacity = 0; var titleLayout = function() { this.call(svgTextUtils.convertToTspans, gd); // TODO: html/mathjax // TODO: center title }; var title = polarPlotSVG.select('.title-group text') .call(titleLayout); if(gd._context.edits.titleText) { var placeholderText = Lib._(gd, 'Click to enter Plot title'); if(!txt || txt === placeholderText) { opacity = 0.2; // placeholder is not going through convertToTspans // so needs explicit data-unformatted title.attr({'data-unformatted': placeholderText}) .text(placeholderText) .style({opacity: opacity}) .on('mouseover.opacity', function() { d3.select(this).transition().duration(100) .style('opacity', 1); }) .on('mouseout.opacity', function() { d3.select(this).transition().duration(1000) .style('opacity', 0); }); } var setContenteditable = function() { this.call(svgTextUtils.makeEditable, {gd: gd}) .on('edit', function(text) { gd.framework({layout: {title: {text: text}}}); this.text(text) .call(titleLayout); this.call(setContenteditable); }) .on('cancel', function() { var txt = this.attr('data-unformatted'); this.text(txt).call(titleLayout); }); }; title.call(setContenteditable); } gd._context.setBackground(gd, gd._fullLayout.paper_bgcolor); Plots.addLinks(gd); return Promise.resolve(); } // convenience function to force a full redraw, mostly for use by plotly.js function redraw(gd) { gd = Lib.getGraphDiv(gd); if(!Lib.isPlotDiv(gd)) { throw new Error('This element is not a Plotly plot: ' + gd); } helpers.cleanData(gd.data); helpers.cleanLayout(gd.layout); gd.calcdata = undefined; return exports.plot(gd).then(function() { gd.emit('plotly_redraw'); return gd; }); } /** * Convenience function to make idempotent plot option obvious to users. * * @param gd * @param {Object[]} data * @param {Object} layout * @param {Object} config */ function newPlot(gd, data, layout, config) { gd = Lib.getGraphDiv(gd); // remove gl contexts Plots.cleanPlot([], {}, gd._fullData || [], gd._fullLayout || {}); Plots.purge(gd); return exports.plot(gd, data, layout, config); } /** * Wrap negative indicies to their positive counterparts. * * @param {Number[]} indices An array of indices * @param {Number} maxIndex The maximum index allowable (arr.length - 1) */ function positivifyIndices(indices, maxIndex) { var parentLength = maxIndex + 1; var positiveIndices = []; var i; var index; for(i = 0; i < indices.length; i++) { index = indices[i]; if(index < 0) { positiveIndices.push(parentLength + index); } else { positiveIndices.push(index); } } return positiveIndices; } /** * Ensures that an index array for manipulating gd.data is valid. * * Intended for use with addTraces, deleteTraces, and moveTraces. * * @param gd * @param indices * @param arrayName */ function assertIndexArray(gd, indices, arrayName) { var i, index; for(i = 0; i < indices.length; i++) { index = indices[i]; // validate that indices are indeed integers if(index !== parseInt(index, 10)) { throw new Error('all values in ' + arrayName + ' must be integers'); } // check that all indices are in bounds for given gd.data array length if(index >= gd.data.length || index < -gd.data.length) { throw new Error(arrayName + ' must be valid indices for gd.data.'); } // check that indices aren't repeated if(indices.indexOf(index, i + 1) > -1 || index >= 0 && indices.indexOf(-gd.data.length + index) > -1 || index < 0 && indices.indexOf(gd.data.length + index) > -1) { throw new Error('each index in ' + arrayName + ' must be unique.'); } } } /** * Private function used by Plotly.moveTraces to check input args * * @param gd * @param currentIndices * @param newIndices */ function checkMoveTracesArgs(gd, currentIndices, newIndices) { // check that gd has attribute 'data' and 'data' is array if(!Array.isArray(gd.data)) { throw new Error('gd.data must be an array.'); } // validate currentIndices array if(typeof currentIndices === 'undefined') { throw new Error('currentIndices is a required argument.'); } else if(!Array.isArray(currentIndices)) { currentIndices = [currentIndices]; } assertIndexArray(gd, currentIndices, 'currentIndices'); // validate newIndices array if it exists if(typeof newIndices !== 'undefined' && !Array.isArray(newIndices)) { newIndices = [newIndices]; } if(typeof newIndices !== 'undefined') { assertIndexArray(gd, newIndices, 'newIndices'); } // check currentIndices and newIndices are the same length if newIdices exists if(typeof newIndices !== 'undefined' && currentIndices.length !== newIndices.length) { throw new Error('current and new indices must be of equal length.'); } } /** * A private function to reduce the type checking clutter in addTraces. * * @param gd * @param traces * @param newIndices */ function checkAddTracesArgs(gd, traces, newIndices) { var i, value; // check that gd has attribute 'data' and 'data' is array if(!Array.isArray(gd.data)) { throw new Error('gd.data must be an array.'); } // make sure traces exists if(typeof traces === 'undefined') { throw new Error('traces must be defined.'); } // make sure traces is an array if(!Array.isArray(traces)) { traces = [traces]; } // make sure each value in traces is an object for(i = 0; i < traces.length; i++) { value = traces[i]; if(typeof value !== 'object' || (Array.isArray(value) || value === null)) { throw new Error('all values in traces array must be non-array objects'); } } // make sure we have an index for each trace if(typeof newIndices !== 'undefined' && !Array.isArray(newIndices)) { newIndices = [newIndices]; } if(typeof newIndices !== 'undefined' && newIndices.length !== traces.length) { throw new Error( 'if indices is specified, traces.length must equal indices.length' ); } } /** * A private function to reduce the type checking clutter in spliceTraces. * Get all update Properties from gd.data. Validate inputs and outputs. * Used by prependTrace and extendTraces * * @param gd * @param update * @param indices * @param maxPoints */ function assertExtendTracesArgs(gd, update, indices, maxPoints) { var maxPointsIsObject = Lib.isPlainObject(maxPoints); if(!Array.isArray(gd.data)) { throw new Error('gd.data must be an array'); } if(!Lib.isPlainObject(update)) { throw new Error('update must be a key:value object'); } if(typeof indices === 'undefined') { throw new Error('indices must be an integer or array of integers'); } assertIndexArray(gd, indices, 'indices'); for(var key in update) { /* * Verify that the attribute to be updated contains as many trace updates * as indices. Failure must result in throw and no-op */ if(!Array.isArray(update[key]) || update[key].length !== indices.length) { throw new Error('attribute ' + key + ' must be an array of length equal to indices array length'); } /* * if maxPoints is an object it must match keys and array lengths of 'update' 1:1 */ if(maxPointsIsObject && (!(key in maxPoints) || !Array.isArray(maxPoints[key]) || maxPoints[key].length !== update[key].length)) { throw new Error('when maxPoints is set as a key:value object it must contain a 1:1 ' + 'corrispondence with the keys and number of traces in the update object'); } } } /** * A private function to reduce the type checking clutter in spliceTraces. * * @param {Object|HTMLDivElement} gd * @param {Object} update * @param {Number[]} indices * @param {Number||Object} maxPoints * @return {Object[]} */ function getExtendProperties(gd, update, indices, maxPoints) { var maxPointsIsObject = Lib.isPlainObject(maxPoints); var updateProps = []; var trace, target, prop, insert, maxp; // allow scalar index to represent a single trace position if(!Array.isArray(indices)) indices = [indices]; // negative indices are wrapped around to their positive value. Equivalent to python indexing. indices = positivifyIndices(indices, gd.data.length - 1); // loop through all update keys and traces and harvest validated data. for(var key in update) { for(var j = 0; j < indices.length; j++) { /* * Choose the trace indexed by the indices map argument and get the prop setter-getter * instance that references the key and value for this particular trace. */ trace = gd.data[indices[j]]; prop = nestedProperty(trace, key); /* * Target is the existing gd.data.trace.dataArray value like "x" or "marker.size" * Target must exist as an Array to allow the extend operation to be performed. */ target = prop.get(); insert = update[key][j]; if(!Lib.isArrayOrTypedArray(insert)) { throw new Error('attribute: ' + key + ' index: ' + j + ' must be an array'); } if(!Lib.isArrayOrTypedArray(target)) { throw new Error('cannot extend missing or non-array attribute: ' + key); } if(target.constructor !== insert.constructor) { throw new Error('cannot extend array with an array of a different type: ' + key); } /* * maxPoints may be an object map or a scalar. If object select the key:value, else * Use the scalar maxPoints for all key and trace combinations. */ maxp = maxPointsIsObject ? maxPoints[key][j] : maxPoints; // could have chosen null here, -1 just tells us to not take a window if(!isNumeric(maxp)) maxp = -1; /* * Wrap the nestedProperty in an object containing required data * for lengthening and windowing this particular trace - key combination. * Flooring maxp mirrors the behaviour of floats in the Array.slice JSnative function. */ updateProps.push({ prop: prop, target: target, insert: insert, maxp: Math.floor(maxp) }); } } // all target and insertion data now validated return updateProps; } /** * A private function to key Extend and Prepend traces DRY * * @param {Object|HTMLDivElement} gd * @param {Object} update * @param {Number[]} indices * @param {Number||Object} maxPoints * @param {Function} updateArray * @return {Object} */ function spliceTraces(gd, update, indices, maxPoints, updateArray) { assertExtendTracesArgs(gd, update, indices, maxPoints); var updateProps = getExtendProperties(gd, update, indices, maxPoints); var undoUpdate = {}; var undoPoints = {}; for(var i = 0; i < updateProps.length; i++) { var prop = updateProps[i].prop; var maxp = updateProps[i].maxp; // return new array and remainder var out = updateArray(updateProps[i].target, updateProps[i].insert, maxp); prop.set(out[0]); // build the inverse update object for the undo operation if(!Array.isArray(undoUpdate[prop.astr])) undoUpdate[prop.astr] = []; undoUpdate[prop.astr].push(out[1]); // build the matching maxPoints undo object containing original trace lengths if(!Array.isArray(undoPoints[prop.astr])) undoPoints[prop.astr] = []; undoPoints[prop.astr].push(updateProps[i].target.length); } return {update: undoUpdate, maxPoints: undoPoints}; } function concatTypedArray(arr0, arr1) { var arr2 = new arr0.constructor(arr0.length + arr1.length); arr2.set(arr0); arr2.set(arr1, arr0.length); return arr2; } /** * extend && prepend traces at indices with update arrays, window trace lengths to maxPoints * * Extend and Prepend have identical APIs. Prepend inserts an array at the head while Extend * inserts an array off the tail. Prepend truncates the tail of the array - counting maxPoints * from the head, whereas Extend truncates the head of the array, counting backward maxPoints * from the tail. * * If maxPoints is undefined, nonNumeric, negative or greater than extended trace length no * truncation / windowing will be performed. If its zero, well the whole trace is truncated. * * @param {Object|HTMLDivElement} gd The graph div * @param {Object} update The key:array map of target attributes to extend * @param {Number|Number[]} indices The locations of traces to be extended * @param {Number|Object} [maxPoints] Number of points for trace window after lengthening. * */ function extendTraces(gd, update, indices, maxPoints) { gd = Lib.getGraphDiv(gd); function updateArray(target, insert, maxp) { var newArray, remainder; if(Lib.isTypedArray(target)) { if(maxp < 0) { var none = new target.constructor(0); var both = concatTypedArray(target, insert); if(maxp < 0) { newArray = both; remainder = none; } else { newArray = none; remainder = both; } } else { newArray = new target.constructor(maxp); remainder = new target.constructor(target.length + insert.length - maxp); if(maxp === insert.length) { newArray.set(insert); remainder.set(target); } else if(maxp < insert.length) { var numberOfItemsFromInsert = insert.length - maxp; newArray.set(insert.subarray(numberOfItemsFromInsert)); remainder.set(target); remainder.set(insert.subarray(0, numberOfItemsFromInsert), target.length); } else { var numberOfItemsFromTarget = maxp - insert.length; var targetBegin = target.length - numberOfItemsFromTarget; newArray.set(target.subarray(targetBegin)); newArray.set(insert, numberOfItemsFromTarget); remainder.set(target.subarray(0, targetBegin)); } } } else { newArray = target.concat(insert); remainder = (maxp >= 0 && maxp < newArray.length) ? newArray.splice(0, newArray.length - maxp) : []; } return [newArray, remainder]; } var undo = spliceTraces(gd, update, indices, maxPoints, updateArray); var promise = exports.redraw(gd); var undoArgs = [gd, undo.update, indices, undo.maxPoints]; Queue.add(gd, exports.prependTraces, undoArgs, extendTraces, arguments); return promise; } function prependTraces(gd, update, indices, maxPoints) { gd = Lib.getGraphDiv(gd); function updateArray(target, insert, maxp) { var newArray, remainder; if(Lib.isTypedArray(target)) { if(maxp <= 0) { var none = new target.constructor(0); var both = concatTypedArray(insert, target); if(maxp < 0) { newArray = both; remainder = none; } else { newArray = none; remainder = both; } } else { newArray = new target.constructor(maxp); remainder = new target.constructor(target.length + insert.length - maxp); if(maxp === insert.length) { newArray.set(insert); remainder.set(target); } else if(maxp < insert.length) { var numberOfItemsFromInsert = insert.length - maxp; newArray.set(insert.subarray(0, numberOfItemsFromInsert)); remainder.set(insert.subarray(numberOfItemsFromInsert)); remainder.set(target, numberOfItemsFromInsert); } else { var numberOfItemsFromTarget = maxp - insert.length; newArray.set(insert); newArray.set(target.subarray(0, numberOfItemsFromTarget), insert.length); remainder.set(target.subarray(numberOfItemsFromTarget)); } } } else { newArray = insert.concat(target); remainder = (maxp >= 0 && maxp < newArray.length) ? newArray.splice(maxp, newArray.length) : []; } return [newArray, remainder]; } var undo = spliceTraces(gd, update, indices, maxPoints, updateArray); var promise = exports.redraw(gd); var undoArgs = [gd, undo.update, indices, undo.maxPoints]; Queue.add(gd, exports.extendTraces, undoArgs, prependTraces, arguments); return promise; } /** * Add data traces to an existing graph div. * * @param {Object|HTMLDivElement} gd The graph div * @param {Object[]} gd.data The array of traces we're adding to * @param {Object[]|Object} traces The object or array of objects to add * @param {Number[]|Number} [newIndices=[gd.data.length]] Locations to add traces * */ function addTraces(gd, traces, newIndices) { gd = Lib.getGraphDiv(gd); var currentIndices = []; var undoFunc = exports.deleteTraces; var redoFunc = addTraces; var undoArgs = [gd, currentIndices]; var redoArgs = [gd, traces]; // no newIndices here var i; var promise; // all validation is done elsewhere to remove clutter here checkAddTracesArgs(gd, traces, newIndices); // make sure traces is an array if(!Array.isArray(traces)) { traces = [traces]; } // make sure traces do not repeat existing ones traces = traces.map(function(trace) { return Lib.extendFlat({}, trace); }); helpers.cleanData(traces); // add the traces to gd.data (no redrawing yet!) for(i = 0; i < traces.length; i++) { gd.data.push(traces[i]); } // to continue, we need to call moveTraces which requires currentIndices for(i = 0; i < traces.length; i++) { currentIndices.push(-traces.length + i); } // if the user didn't define newIndices, they just want the traces appended // i.e., we can simply redraw and be done if(typeof newIndices === 'undefined') { promise = exports.redraw(gd); Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs); return promise; } // make sure indices is property defined if(!Array.isArray(newIndices)) { newIndices = [newIndices]; } try { // this is redundant, but necessary to not catch later possible errors! checkMoveTracesArgs(gd, currentIndices, newIndices); } catch(error) { // something went wrong, reset gd to be safe and rethrow error gd.data.splice(gd.data.length - traces.length, traces.length); throw error; } // if we're here, the user has defined specific places to place the new traces // this requires some extra work that moveTraces will do Queue.startSequence(gd); Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs); promise = exports.moveTraces(gd, currentIndices, newIndices); Queue.stopSequence(gd); return promise; } /** * Delete traces at `indices` from gd.data array. * * @param {Object|HTMLDivElement} gd The graph div * @param {Object[]} gd.data The array of traces we're removing from * @param {Number|Number[]} indices The indices */ function deleteTraces(gd, indices) { gd = Lib.getGraphDiv(gd); var traces = []; var undoFunc = exports.addTraces; var redoFunc = deleteTraces; var undoArgs = [gd, traces, indices]; var redoArgs = [gd, indices]; var i; var deletedTrace; // make sure indices are defined if(typeof indices === 'undefined') { throw new Error('indices must be an integer or array of integers.'); } else if(!Array.isArray(indices)) { indices = [indices]; } assertIndexArray(gd, indices, 'indices'); // convert negative indices to positive indices indices = positivifyIndices(indices, gd.data.length - 1); // we want descending here so that splicing later doesn't affect indexing indices.sort(Lib.sorterDes); for(i = 0; i < indices.length; i += 1) { deletedTrace = gd.data.splice(indices[i], 1)[0]; traces.push(deletedTrace); } var promise = exports.redraw(gd); Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs); return promise; } /** * Move traces at currentIndices array to locations in newIndices array. * * If newIndices is omitted, currentIndices will be moved to the end. E.g., * these are equivalent: * * Plotly.moveTraces(gd, [1, 2, 3], [-3, -2, -1]) * Plotly.moveTraces(gd, [1, 2, 3]) * * @param {Object|HTMLDivElement} gd The graph div * @param {Object[]} gd.data The array of traces we're removing from * @param {Number|Number[]} currentIndices The locations of traces to be moved * @param {Number|Number[]} [newIndices] The locations to move traces to * * Example calls: * * // move trace i to location x * Plotly.moveTraces(gd, i, x) * * // move trace i to end of array * Plotly.moveTraces(gd, i) * * // move traces i, j, k to end of array (i != j != k) * Plotly.moveTraces(gd, [i, j, k]) * * // move traces [i, j, k] to [x, y, z] (i != j != k) (x != y != z) * Plotly.moveTraces(gd, [i, j, k], [x, y, z]) * * // reorder all traces (assume there are 5--a, b, c, d, e) * Plotly.moveTraces(gd, [b, d, e, a, c]) // same as 'move to end' */ function moveTraces(gd, currentIndices, newIndices) { gd = Lib.getGraphDiv(gd); var newData = []; var movingTraceMap = []; var undoFunc = moveTraces; var redoFunc = moveTraces; var undoArgs = [gd, newIndices, currentIndices]; var redoArgs = [gd, currentIndices, newIndices]; var i; // to reduce complexity here, check args elsewhere // this throws errors where appropriate checkMoveTracesArgs(gd, currentIndices, newIndices); // make sure currentIndices is an array currentIndices = Array.isArray(currentIndices) ? currentIndices : [currentIndices]; // if undefined, define newIndices to point to the end of gd.data array if(typeof newIndices === 'undefined') { newIndices = []; for(i = 0; i < currentIndices.length; i++) { newIndices.push(-currentIndices.length + i); } } // make sure newIndices is an array if it's user-defined newIndices = Array.isArray(newIndices) ? newIndices : [newIndices]; // convert negative indices to positive indices (they're the same length) currentIndices = positivifyIndices(currentIndices, gd.data.length - 1); newIndices = positivifyIndices(newIndices, gd.data.length - 1); // at this point, we've coerced the index arrays into predictable forms // get the traces that aren't being moved around for(i = 0; i < gd.data.length; i++) { // if index isn't in currentIndices, include it in ignored! if(currentIndices.indexOf(i) === -1) { newData.push(gd.data[i]); } } // get a mapping of indices to moving traces for(i = 0; i < currentIndices.length; i++) { movingTraceMap.push({newIndex: newIndices[i], trace: gd.data[currentIndices[i]]}); } // reorder this mapping by newIndex, ascending movingTraceMap.sort(function(a, b) { return a.newIndex - b.newIndex; }); // now, add the moving traces back in, in order! for(i = 0; i < movingTraceMap.length; i += 1) { newData.splice(movingTraceMap[i].newIndex, 0, movingTraceMap[i].trace); } gd.data = newData; var promise = exports.redraw(gd); Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs); return promise; } /** * restyle: update trace attributes of an existing plot * * Can be called two ways. * * Signature 1: * @param {String | HTMLDivElement} gd * the id or DOM element of the graph container div * @param {String} astr * attribute string (like `'marker.symbol'`) to update * @param {*} val * value to give this attribute * @param {Number[] | Number} [traces] * integer or array of integers for the traces to alter (all if omitted) * * Signature 2: * @param {String | HTMLDivElement} gd * (as in signature 1) * @param {Object} aobj * attribute object `{astr1: val1, astr2: val2 ...}` * allows setting multiple attributes simultaneously * @param {Number[] | Number} [traces] * (as in signature 1) * * `val` (or `val1`, `val2` ... in the object form) can be an array, * to apply different values to each trace. * * If the array is too short, it will wrap around (useful for * style files that want to specify cyclical default values). */ function restyle(gd, astr, val, _traces) { gd = Lib.getGraphDiv(gd); helpers.clearPromiseQueue(gd); var aobj = {}; if(typeof astr === 'string') aobj[astr] = val; else if(Lib.isPlainObject(astr)) { // the 3-arg form aobj = Lib.extendFlat({}, astr); if(_traces === undefined) _traces = val; } else { Lib.warn('Restyle fail.', astr, val, _traces); return Promise.reject(); } if(Object.keys(aobj).length) gd.changed = true; var traces = helpers.coerceTraceIndices(gd, _traces); var specs = _restyle(gd, aobj, traces); var flags = specs.flags; // clear calcdata and/or axis types if required so they get regenerated if(flags.calc) gd.calcdata = undefined; if(flags.clearAxisTypes) helpers.clearAxisTypes(gd, traces, {}); // fill in redraw sequence var seq = []; if(flags.fullReplot) { seq.push(exports.plot); } else { seq.push(Plots.previousPromises); // maybe only call Plots.supplyDataDefaults in the splom case, // to skip over long and slow axes defaults Plots.supplyDefaults(gd); if(flags.markerSize) { Plots.doCalcdata(gd); addAxRangeSequence(seq); // TODO // if all axes have autorange:false, then // proceed to subroutines.doTraceStyle(), // otherwise we must go through addAxRangeSequence, // which in general must redraws 'all' axes } if(flags.style) seq.push(subroutines.doTraceStyle); if(flags.colorbars) seq.push(subroutines.doColorBars); seq.push(emitAfterPlot); } seq.push(Plots.rehover, Plots.redrag); Queue.add(gd, restyle, [gd, specs.undoit, specs.traces], restyle, [gd, specs.redoit, specs.traces] ); var plotDone = Lib.syncOrAsync(seq, gd); if(!plotDone || !plotDone.then) plotDone = Promise.resolve(); return plotDone.then(function() { gd.emit('plotly_restyle', specs.eventData); return gd; }); } // for undo: undefined initial vals must be turned into nulls // so that we unset rather than ignore them function undefinedToNull(val) { if(val === undefined) return null; return val; } /** * Factory function to wrap nestedProperty with GUI edits if necessary * with GUI edits we add an optional prefix to the nestedProperty constructor * to prepend to the attribute string in the preGUI store. */ function makeNP(preGUI, guiEditFlag) { if(!guiEditFlag) return nestedProperty; return function(container, attr, prefix) { var np = nestedProperty(container, attr); var npSet = np.set; np.set = function(val) { var fullAttr = (prefix || '') + attr; storeCurrent(fullAttr, np.get(), val, preGUI); npSet(val); }; return np; }; } function storeCurrent(attr, val, newVal, preGUI) { if(Array.isArray(val) || Array.isArray(newVal)) { var arrayVal = Array.isArray(val) ? val : []; var arrayNew = Array.isArray(newVal) ? newVal : []; var maxLen = Math.max(arrayVal.length, arrayNew.length); for(var i = 0; i < maxLen; i++) { storeCurrent(attr + '[' + i + ']', arrayVal[i], arrayNew[i], preGUI); } } else if(Lib.isPlainObject(val) || Lib.isPlainObject(newVal)) { var objVal = Lib.isPlainObject(val) ? val : {}; var objNew = Lib.isPlainObject(newVal) ? newVal : {}; var objBoth = Lib.extendFlat({}, objVal, objNew); for(var key in objBoth) { storeCurrent(attr + '.' + key, objVal[key], objNew[key], preGUI); } } else if(preGUI[attr] === undefined) { preGUI[attr] = undefinedToNull(val); } } /** * storeDirectGUIEdit: for routines that skip restyle/relayout and mock it * by emitting a plotly_restyle or plotly_relayout event, this routine * keeps track of the initial state in _preGUI for use by uirevision * Does *not* apply these changes to data/layout - that's the responsibility * of the calling routine. * * @param {object} container: the input attributes container (eg `layout` or a `trace`) * @param {object} preGUI: where original values should be stored, either * `layout._preGUI` or `layout._tracePreGUI[uid]` * @param {object} edits: the {attr: val} object as normally passed to `relayout` etc */ function _storeDirectGUIEdit(contai