diff --git a/src/core.js b/src/core.js index 143ae88bdaf..c5343b1a7dd 100644 --- a/src/core.js +++ b/src/core.js @@ -32,6 +32,7 @@ exports.newPlot = Plotly.newPlot; exports.restyle = Plotly.restyle; exports.relayout = Plotly.relayout; exports.redraw = Plotly.redraw; +exports.update = Plotly.update; exports.extendTraces = Plotly.extendTraces; exports.prependTraces = Plotly.prependTraces; exports.addTraces = Plotly.addTraces; diff --git a/src/plot_api/helpers.js b/src/plot_api/helpers.js new file mode 100644 index 00000000000..35d9a0495f1 --- /dev/null +++ b/src/plot_api/helpers.js @@ -0,0 +1,414 @@ +/** +* Copyright 2012-2016, 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 isNumeric = require('fast-isnumeric'); +var m4FromQuat = require('gl-mat4/fromQuat'); + +var Registry = require('../registry'); +var Lib = require('../lib'); +var Plots = require('../plots/plots'); +var Axes = require('../plots/cartesian/axes'); +var Color = require('../components/color'); + + +// Get the container div: we store all variables for this plot as +// properties of this div +// some callers send this in by DOM element, others by id (string) +exports.getGraphDiv = function(gd) { + var gdElement; + + if(typeof gd === 'string') { + gdElement = document.getElementById(gd); + + if(gdElement === null) { + throw new Error('No DOM element with id \'' + gd + '\' exists on the page.'); + } + + return gdElement; + } + else if(gd === null || gd === undefined) { + throw new Error('DOM element provided is null or undefined'); + } + + return gd; // otherwise assume that gd is a DOM element +}; + +// clear the promise queue if one of them got rejected +exports.clearPromiseQueue = function(gd) { + if(Array.isArray(gd._promises) && gd._promises.length > 0) { + Lib.log('Clearing previous rejected promises from queue.'); + } + + gd._promises = []; +}; + +// make a few changes to the layout right away +// before it gets used for anything +// backward compatibility and cleanup of nonstandard options +exports.cleanLayout = function(layout) { + var i, j; + + if(!layout) layout = {}; + + // cannot have (x|y)axis1, numbering goes axis, axis2, axis3... + if(layout.xaxis1) { + if(!layout.xaxis) layout.xaxis = layout.xaxis1; + delete layout.xaxis1; + } + if(layout.yaxis1) { + if(!layout.yaxis) layout.yaxis = layout.yaxis1; + delete layout.yaxis1; + } + + var axList = Axes.list({_fullLayout: layout}); + for(i = 0; i < axList.length; i++) { + var ax = axList[i]; + if(ax.anchor && ax.anchor !== 'free') { + ax.anchor = Axes.cleanId(ax.anchor); + } + if(ax.overlaying) ax.overlaying = Axes.cleanId(ax.overlaying); + + // old method of axis type - isdate and islog (before category existed) + if(!ax.type) { + if(ax.isdate) ax.type = 'date'; + else if(ax.islog) ax.type = 'log'; + else if(ax.isdate === false && ax.islog === false) ax.type = 'linear'; + } + if(ax.autorange === 'withzero' || ax.autorange === 'tozero') { + ax.autorange = true; + ax.rangemode = 'tozero'; + } + delete ax.islog; + delete ax.isdate; + delete ax.categories; // replaced by _categories + + // prune empty domain arrays made before the new nestedProperty + if(emptyContainer(ax, 'domain')) delete ax.domain; + + // autotick -> tickmode + if(ax.autotick !== undefined) { + if(ax.tickmode === undefined) { + ax.tickmode = ax.autotick ? 'auto' : 'linear'; + } + delete ax.autotick; + } + } + + if(layout.annotations !== undefined && !Array.isArray(layout.annotations)) { + Lib.warn('Annotations must be an array.'); + delete layout.annotations; + } + var annotationsLen = (layout.annotations || []).length; + for(i = 0; i < annotationsLen; i++) { + var ann = layout.annotations[i]; + if(ann.ref) { + if(ann.ref === 'paper') { + ann.xref = 'paper'; + ann.yref = 'paper'; + } + else if(ann.ref === 'data') { + ann.xref = 'x'; + ann.yref = 'y'; + } + delete ann.ref; + } + cleanAxRef(ann, 'xref'); + cleanAxRef(ann, 'yref'); + } + + if(layout.shapes !== undefined && !Array.isArray(layout.shapes)) { + Lib.warn('Shapes must be an array.'); + delete layout.shapes; + } + var shapesLen = (layout.shapes || []).length; + for(i = 0; i < shapesLen; i++) { + var shape = layout.shapes[i]; + cleanAxRef(shape, 'xref'); + cleanAxRef(shape, 'yref'); + } + + var legend = layout.legend; + if(legend) { + // check for old-style legend positioning (x or y is +/- 100) + if(legend.x > 3) { + legend.x = 1.02; + legend.xanchor = 'left'; + } + else if(legend.x < -2) { + legend.x = -0.02; + legend.xanchor = 'right'; + } + + if(legend.y > 3) { + legend.y = 1.02; + legend.yanchor = 'bottom'; + } + else if(legend.y < -2) { + legend.y = -0.02; + legend.yanchor = 'top'; + } + } + + /* + * Moved from rotate -> orbit for dragmode + */ + if(layout.dragmode === 'rotate') layout.dragmode = 'orbit'; + + // cannot have scene1, numbering goes scene, scene2, scene3... + if(layout.scene1) { + if(!layout.scene) layout.scene = layout.scene1; + delete layout.scene1; + } + + /* + * Clean up Scene layouts + */ + var sceneIds = Plots.getSubplotIds(layout, 'gl3d'); + for(i = 0; i < sceneIds.length; i++) { + var scene = layout[sceneIds[i]]; + + // clean old Camera coords + var cameraposition = scene.cameraposition; + if(Array.isArray(cameraposition) && cameraposition[0].length === 4) { + var rotation = cameraposition[0], + center = cameraposition[1], + radius = cameraposition[2], + mat = m4FromQuat([], rotation), + eye = []; + + for(j = 0; j < 3; ++j) { + eye[j] = center[i] + radius * mat[2 + 4 * j]; + } + + scene.camera = { + eye: {x: eye[0], y: eye[1], z: eye[2]}, + center: {x: center[0], y: center[1], z: center[2]}, + up: {x: mat[1], y: mat[5], z: mat[9]} + }; + + delete scene.cameraposition; + } + } + + // sanitize rgb(fractions) and rgba(fractions) that old tinycolor + // supported, but new tinycolor does not because they're not valid css + Color.clean(layout); + + return layout; +}; + +function cleanAxRef(container, attr) { + var valIn = container[attr], + axLetter = attr.charAt(0); + if(valIn && valIn !== 'paper') { + container[attr] = Axes.cleanId(valIn, axLetter); + } +} + +// Make a few changes to the data right away +// before it gets used for anything +exports.cleanData = function(data, existingData) { + + // Enforce unique IDs + var suids = [], // seen uids --- so we can weed out incoming repeats + uids = data.concat(Array.isArray(existingData) ? existingData : []) + .filter(function(trace) { return 'uid' in trace; }) + .map(function(trace) { return trace.uid; }); + + for(var tracei = 0; tracei < data.length; tracei++) { + var trace = data[tracei]; + var i; + + // assign uids to each trace and detect collisions. + if(!('uid' in trace) || suids.indexOf(trace.uid) !== -1) { + var newUid; + + for(i = 0; i < 100; i++) { + newUid = Lib.randstr(uids); + if(suids.indexOf(newUid) === -1) break; + } + trace.uid = Lib.randstr(uids); + uids.push(trace.uid); + } + // keep track of already seen uids, so that if there are + // doubles we force the trace with a repeat uid to + // acquire a new one + suids.push(trace.uid); + + // BACKWARD COMPATIBILITY FIXES + + // use xbins to bin data in x, and ybins to bin data in y + if(trace.type === 'histogramy' && 'xbins' in trace && !('ybins' in trace)) { + trace.ybins = trace.xbins; + delete trace.xbins; + } + + // error_y.opacity is obsolete - merge into color + if(trace.error_y && 'opacity' in trace.error_y) { + var dc = Color.defaults, + yeColor = trace.error_y.color || + (Registry.traceIs(trace, 'bar') ? Color.defaultLine : dc[tracei % dc.length]); + trace.error_y.color = Color.addOpacity( + Color.rgb(yeColor), + Color.opacity(yeColor) * trace.error_y.opacity); + delete trace.error_y.opacity; + } + + // convert bardir to orientation, and put the data into + // the axes it's eventually going to be used with + if('bardir' in trace) { + if(trace.bardir === 'h' && (Registry.traceIs(trace, 'bar') || + trace.type.substr(0, 9) === 'histogram')) { + trace.orientation = 'h'; + exports.swapXYData(trace); + } + delete trace.bardir; + } + + // now we have only one 1D histogram type, and whether + // it uses x or y data depends on trace.orientation + if(trace.type === 'histogramy') exports.swapXYData(trace); + if(trace.type === 'histogramx' || trace.type === 'histogramy') { + trace.type = 'histogram'; + } + + // scl->scale, reversescl->reversescale + if('scl' in trace) { + trace.colorscale = trace.scl; + delete trace.scl; + } + if('reversescl' in trace) { + trace.reversescale = trace.reversescl; + delete trace.reversescl; + } + + // axis ids x1 -> x, y1-> y + if(trace.xaxis) trace.xaxis = Axes.cleanId(trace.xaxis, 'x'); + if(trace.yaxis) trace.yaxis = Axes.cleanId(trace.yaxis, 'y'); + + // scene ids scene1 -> scene + if(Registry.traceIs(trace, 'gl3d') && trace.scene) { + trace.scene = Plots.subplotsRegistry.gl3d.cleanId(trace.scene); + } + + if(!Registry.traceIs(trace, 'pie')) { + if(Array.isArray(trace.textposition)) { + trace.textposition = trace.textposition.map(cleanTextPosition); + } + else if(trace.textposition) { + trace.textposition = cleanTextPosition(trace.textposition); + } + } + + // fix typo in colorscale definition + if(Registry.traceIs(trace, '2dMap')) { + if(trace.colorscale === 'YIGnBu') trace.colorscale = 'YlGnBu'; + if(trace.colorscale === 'YIOrRd') trace.colorscale = 'YlOrRd'; + } + if(Registry.traceIs(trace, 'markerColorscale') && trace.marker) { + var cont = trace.marker; + if(cont.colorscale === 'YIGnBu') cont.colorscale = 'YlGnBu'; + if(cont.colorscale === 'YIOrRd') cont.colorscale = 'YlOrRd'; + } + + // fix typo in surface 'highlight*' definitions + if(trace.type === 'surface' && Lib.isPlainObject(trace.contours)) { + var dims = ['x', 'y', 'z']; + + for(i = 0; i < dims.length; i++) { + var opts = trace.contours[dims[i]]; + + if(!Lib.isPlainObject(opts)) continue; + + if(opts.highlightColor) { + opts.highlightcolor = opts.highlightColor; + delete opts.highlightColor; + } + + if(opts.highlightWidth) { + opts.highlightwidth = opts.highlightWidth; + delete opts.highlightWidth; + } + } + } + + // prune empty containers made before the new nestedProperty + if(emptyContainer(trace, 'line')) delete trace.line; + if('marker' in trace) { + if(emptyContainer(trace.marker, 'line')) delete trace.marker.line; + if(emptyContainer(trace, 'marker')) delete trace.marker; + } + + // sanitize rgb(fractions) and rgba(fractions) that old tinycolor + // supported, but new tinycolor does not because they're not valid css + Color.clean(trace); + } +}; + +// textposition - support partial attributes (ie just 'top') +// and incorrect use of middle / center etc. +function cleanTextPosition(textposition) { + var posY = 'middle', + posX = 'center'; + if(textposition.indexOf('top') !== -1) posY = 'top'; + else if(textposition.indexOf('bottom') !== -1) posY = 'bottom'; + + if(textposition.indexOf('left') !== -1) posX = 'left'; + else if(textposition.indexOf('right') !== -1) posX = 'right'; + + return posY + ' ' + posX; +} + +function emptyContainer(outer, innerStr) { + return (innerStr in outer) && + (typeof outer[innerStr] === 'object') && + (Object.keys(outer[innerStr]).length === 0); +} + + +// swap all the data and data attributes associated with x and y +exports.swapXYData = function(trace) { + var i; + Lib.swapAttrs(trace, ['?', '?0', 'd?', '?bins', 'nbins?', 'autobin?', '?src', 'error_?']); + if(Array.isArray(trace.z) && Array.isArray(trace.z[0])) { + if(trace.transpose) delete trace.transpose; + else trace.transpose = true; + } + if(trace.error_x && trace.error_y) { + var errorY = trace.error_y, + copyYstyle = ('copy_ystyle' in errorY) ? errorY.copy_ystyle : + !(errorY.color || errorY.thickness || errorY.width); + Lib.swapAttrs(trace, ['error_?.copy_ystyle']); + if(copyYstyle) { + Lib.swapAttrs(trace, ['error_?.color', 'error_?.thickness', 'error_?.width']); + } + } + if(trace.hoverinfo) { + var hoverInfoParts = trace.hoverinfo.split('+'); + for(i = 0; i < hoverInfoParts.length; i++) { + if(hoverInfoParts[i] === 'x') hoverInfoParts[i] = 'y'; + else if(hoverInfoParts[i] === 'y') hoverInfoParts[i] = 'x'; + } + trace.hoverinfo = hoverInfoParts.join('+'); + } +}; + +// coerce traceIndices input to array of trace indices +exports.coerceTraceIndices = function(gd, traceIndices) { + if(isNumeric(traceIndices)) { + return [traceIndices]; + } + else if(!Array.isArray(traceIndices) || !traceIndices.length) { + return gd.data.map(function(_, i) { return i; }); + } + + return traceIndices; +}; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index c3a20b1458f..2a7f445708b 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -11,7 +11,6 @@ var d3 = require('d3'); -var m4FromQuat = require('gl-mat4/fromQuat'); var isNumeric = require('fast-isnumeric'); var Plotly = require('../plotly'); @@ -24,14 +23,14 @@ var Plots = require('../plots/plots'); var Fx = require('../plots/cartesian/graph_interact'); var Polar = require('../plots/polar'); -var Color = require('../components/color'); var Drawing = require('../components/drawing'); var ErrorBars = require('../components/errorbars'); -var Titles = require('../components/titles'); -var ModeBar = require('../components/modebar'); var xmlnsNamespaces = require('../constants/xmlns_namespaces'); var svgTextUtils = require('../lib/svg_text_utils'); +var helpers = require('./helpers'); +var subroutines = require('./subroutines'); + /** * Main plot-creation function @@ -50,8 +49,7 @@ var svgTextUtils = require('../lib/svg_text_utils'); * */ Plotly.plot = function(gd, data, layout, config) { - - gd = getGraphDiv(gd); + gd = helpers.getGraphDiv(gd); // Events.init is idempotent and bails early if gd has already been init'd Events.init(gd); @@ -92,7 +90,7 @@ Plotly.plot = function(gd, data, layout, config) { // 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)) { - cleanData(data, gd.data); + helpers.cleanData(data, gd.data); if(graphWasEmpty) gd.data = data; else gd.data.push.apply(gd.data, data); @@ -103,7 +101,7 @@ Plotly.plot = function(gd, data, layout, config) { gd.empty = false; } - if(!gd.layout || graphWasEmpty) gd.layout = cleanLayout(layout); + if(!gd.layout || graphWasEmpty) gd.layout = helpers.cleanLayout(layout); // if the user is trying to drag the axes, allow new data and layout // to come in but don't allow a replot. @@ -197,7 +195,7 @@ Plotly.plot = function(gd, data, layout, config) { function marginPushersAgain() { // in case the margins changed, draw margin pushers again var seq = JSON.stringify(fullLayout._size) === oldmargins ? - [] : [marginPushers, layoutStyles]; + [] : [marginPushers, subroutines.layoutStyles]; return Lib.syncOrAsync(seq.concat(Fx.init), gd); } @@ -318,7 +316,7 @@ Plotly.plot = function(gd, data, layout, config) { marginPushers, marginPushersAgain, positionAndAutorange, - layoutStyles, + subroutines.layoutStyles, drawAxes, drawData, finalDraw @@ -331,36 +329,6 @@ Plotly.plot = function(gd, data, layout, config) { }); }; -// Get the container div: we store all variables for this plot as -// properties of this div -// some callers send this in by DOM element, others by id (string) -function getGraphDiv(gd) { - var gdElement; - - if(typeof gd === 'string') { - gdElement = document.getElementById(gd); - - if(gdElement === null) { - throw new Error('No DOM element with id \'' + gd + '\' exists on the page.'); - } - - return gdElement; - } - else if(gd === null || gd === undefined) { - throw new Error('DOM element provided is null or undefined'); - } - - return gd; // otherwise assume that gd is a DOM element -} - -// clear the promise queue if one of them got rejected -function clearPromiseQueue(gd) { - if(Array.isArray(gd._promises) && gd._promises.length > 0) { - Lib.log('Clearing previous rejected promises from queue.'); - } - - gd._promises = []; -} function opaqueSetBackground(gd, bgColor) { gd._fullLayout._paperdiv.style('background', 'white'); @@ -497,332 +465,9 @@ function plotPolar(gd, data, layout) { return Promise.resolve(); } -function cleanLayout(layout) { - // make a few changes to the layout right away - // before it gets used for anything - // backward compatibility and cleanup of nonstandard options - var i, j; - - if(!layout) layout = {}; - - // cannot have (x|y)axis1, numbering goes axis, axis2, axis3... - if(layout.xaxis1) { - if(!layout.xaxis) layout.xaxis = layout.xaxis1; - delete layout.xaxis1; - } - if(layout.yaxis1) { - if(!layout.yaxis) layout.yaxis = layout.yaxis1; - delete layout.yaxis1; - } - - var axList = Plotly.Axes.list({_fullLayout: layout}); - for(i = 0; i < axList.length; i++) { - var ax = axList[i]; - if(ax.anchor && ax.anchor !== 'free') { - ax.anchor = Plotly.Axes.cleanId(ax.anchor); - } - if(ax.overlaying) ax.overlaying = Plotly.Axes.cleanId(ax.overlaying); - - // old method of axis type - isdate and islog (before category existed) - if(!ax.type) { - if(ax.isdate) ax.type = 'date'; - else if(ax.islog) ax.type = 'log'; - else if(ax.isdate === false && ax.islog === false) ax.type = 'linear'; - } - if(ax.autorange === 'withzero' || ax.autorange === 'tozero') { - ax.autorange = true; - ax.rangemode = 'tozero'; - } - delete ax.islog; - delete ax.isdate; - delete ax.categories; // replaced by _categories - - // prune empty domain arrays made before the new nestedProperty - if(emptyContainer(ax, 'domain')) delete ax.domain; - - // autotick -> tickmode - if(ax.autotick !== undefined) { - if(ax.tickmode === undefined) { - ax.tickmode = ax.autotick ? 'auto' : 'linear'; - } - delete ax.autotick; - } - } - - if(layout.annotations !== undefined && !Array.isArray(layout.annotations)) { - Lib.warn('Annotations must be an array.'); - delete layout.annotations; - } - var annotationsLen = (layout.annotations || []).length; - for(i = 0; i < annotationsLen; i++) { - var ann = layout.annotations[i]; - if(ann.ref) { - if(ann.ref === 'paper') { - ann.xref = 'paper'; - ann.yref = 'paper'; - } - else if(ann.ref === 'data') { - ann.xref = 'x'; - ann.yref = 'y'; - } - delete ann.ref; - } - cleanAxRef(ann, 'xref'); - cleanAxRef(ann, 'yref'); - } - - if(layout.shapes !== undefined && !Array.isArray(layout.shapes)) { - Lib.warn('Shapes must be an array.'); - delete layout.shapes; - } - var shapesLen = (layout.shapes || []).length; - for(i = 0; i < shapesLen; i++) { - var shape = layout.shapes[i]; - cleanAxRef(shape, 'xref'); - cleanAxRef(shape, 'yref'); - } - - var legend = layout.legend; - if(legend) { - // check for old-style legend positioning (x or y is +/- 100) - if(legend.x > 3) { - legend.x = 1.02; - legend.xanchor = 'left'; - } - else if(legend.x < -2) { - legend.x = -0.02; - legend.xanchor = 'right'; - } - - if(legend.y > 3) { - legend.y = 1.02; - legend.yanchor = 'bottom'; - } - else if(legend.y < -2) { - legend.y = -0.02; - legend.yanchor = 'top'; - } - } - - /* - * Moved from rotate -> orbit for dragmode - */ - if(layout.dragmode === 'rotate') layout.dragmode = 'orbit'; - - // cannot have scene1, numbering goes scene, scene2, scene3... - if(layout.scene1) { - if(!layout.scene) layout.scene = layout.scene1; - delete layout.scene1; - } - - /* - * Clean up Scene layouts - */ - var sceneIds = Plots.getSubplotIds(layout, 'gl3d'); - for(i = 0; i < sceneIds.length; i++) { - var scene = layout[sceneIds[i]]; - - // clean old Camera coords - var cameraposition = scene.cameraposition; - if(Array.isArray(cameraposition) && cameraposition[0].length === 4) { - var rotation = cameraposition[0], - center = cameraposition[1], - radius = cameraposition[2], - mat = m4FromQuat([], rotation), - eye = []; - - for(j = 0; j < 3; ++j) { - eye[j] = center[i] + radius * mat[2 + 4 * j]; - } - - scene.camera = { - eye: {x: eye[0], y: eye[1], z: eye[2]}, - center: {x: center[0], y: center[1], z: center[2]}, - up: {x: mat[1], y: mat[5], z: mat[9]} - }; - - delete scene.cameraposition; - } - } - - // sanitize rgb(fractions) and rgba(fractions) that old tinycolor - // supported, but new tinycolor does not because they're not valid css - Color.clean(layout); - - return layout; -} - -function cleanAxRef(container, attr) { - var valIn = container[attr], - axLetter = attr.charAt(0); - if(valIn && valIn !== 'paper') { - container[attr] = Plotly.Axes.cleanId(valIn, axLetter); - } -} - -// Make a few changes to the data right away -// before it gets used for anything -function cleanData(data, existingData) { - - // Enforce unique IDs - var suids = [], // seen uids --- so we can weed out incoming repeats - uids = data.concat(Array.isArray(existingData) ? existingData : []) - .filter(function(trace) { return 'uid' in trace; }) - .map(function(trace) { return trace.uid; }); - - for(var tracei = 0; tracei < data.length; tracei++) { - var trace = data[tracei]; - var i; - - // assign uids to each trace and detect collisions. - if(!('uid' in trace) || suids.indexOf(trace.uid) !== -1) { - var newUid; - - for(i = 0; i < 100; i++) { - newUid = Lib.randstr(uids); - if(suids.indexOf(newUid) === -1) break; - } - trace.uid = Lib.randstr(uids); - uids.push(trace.uid); - } - // keep track of already seen uids, so that if there are - // doubles we force the trace with a repeat uid to - // acquire a new one - suids.push(trace.uid); - - // BACKWARD COMPATIBILITY FIXES - - // use xbins to bin data in x, and ybins to bin data in y - if(trace.type === 'histogramy' && 'xbins' in trace && !('ybins' in trace)) { - trace.ybins = trace.xbins; - delete trace.xbins; - } - - // error_y.opacity is obsolete - merge into color - if(trace.error_y && 'opacity' in trace.error_y) { - var dc = Color.defaults, - yeColor = trace.error_y.color || - (Registry.traceIs(trace, 'bar') ? Color.defaultLine : dc[tracei % dc.length]); - trace.error_y.color = Color.addOpacity( - Color.rgb(yeColor), - Color.opacity(yeColor) * trace.error_y.opacity); - delete trace.error_y.opacity; - } - - // convert bardir to orientation, and put the data into - // the axes it's eventually going to be used with - if('bardir' in trace) { - if(trace.bardir === 'h' && (Registry.traceIs(trace, 'bar') || - trace.type.substr(0, 9) === 'histogram')) { - trace.orientation = 'h'; - swapXYData(trace); - } - delete trace.bardir; - } - - // now we have only one 1D histogram type, and whether - // it uses x or y data depends on trace.orientation - if(trace.type === 'histogramy') swapXYData(trace); - if(trace.type === 'histogramx' || trace.type === 'histogramy') { - trace.type = 'histogram'; - } - - // scl->scale, reversescl->reversescale - if('scl' in trace) { - trace.colorscale = trace.scl; - delete trace.scl; - } - if('reversescl' in trace) { - trace.reversescale = trace.reversescl; - delete trace.reversescl; - } - - // axis ids x1 -> x, y1-> y - if(trace.xaxis) trace.xaxis = Plotly.Axes.cleanId(trace.xaxis, 'x'); - if(trace.yaxis) trace.yaxis = Plotly.Axes.cleanId(trace.yaxis, 'y'); - - // scene ids scene1 -> scene - if(Registry.traceIs(trace, 'gl3d') && trace.scene) { - trace.scene = Plots.subplotsRegistry.gl3d.cleanId(trace.scene); - } - - if(!Registry.traceIs(trace, 'pie')) { - if(Array.isArray(trace.textposition)) { - trace.textposition = trace.textposition.map(cleanTextPosition); - } - else if(trace.textposition) { - trace.textposition = cleanTextPosition(trace.textposition); - } - } - - // fix typo in colorscale definition - if(Registry.traceIs(trace, '2dMap')) { - if(trace.colorscale === 'YIGnBu') trace.colorscale = 'YlGnBu'; - if(trace.colorscale === 'YIOrRd') trace.colorscale = 'YlOrRd'; - } - if(Registry.traceIs(trace, 'markerColorscale') && trace.marker) { - var cont = trace.marker; - if(cont.colorscale === 'YIGnBu') cont.colorscale = 'YlGnBu'; - if(cont.colorscale === 'YIOrRd') cont.colorscale = 'YlOrRd'; - } - - // fix typo in surface 'highlight*' definitions - if(trace.type === 'surface' && Lib.isPlainObject(trace.contours)) { - var dims = ['x', 'y', 'z']; - - for(i = 0; i < dims.length; i++) { - var opts = trace.contours[dims[i]]; - - if(!Lib.isPlainObject(opts)) continue; - - if(opts.highlightColor) { - opts.highlightcolor = opts.highlightColor; - delete opts.highlightColor; - } - - if(opts.highlightWidth) { - opts.highlightwidth = opts.highlightWidth; - delete opts.highlightWidth; - } - } - } - - // prune empty containers made before the new nestedProperty - if(emptyContainer(trace, 'line')) delete trace.line; - if('marker' in trace) { - if(emptyContainer(trace.marker, 'line')) delete trace.marker.line; - if(emptyContainer(trace, 'marker')) delete trace.marker; - } - - // sanitize rgb(fractions) and rgba(fractions) that old tinycolor - // supported, but new tinycolor does not because they're not valid css - Color.clean(trace); - } -} - -// textposition - support partial attributes (ie just 'top') -// and incorrect use of middle / center etc. -function cleanTextPosition(textposition) { - var posY = 'middle', - posX = 'center'; - if(textposition.indexOf('top') !== -1) posY = 'top'; - else if(textposition.indexOf('bottom') !== -1) posY = 'bottom'; - - if(textposition.indexOf('left') !== -1) posX = 'left'; - else if(textposition.indexOf('right') !== -1) posX = 'right'; - - return posY + ' ' + posX; -} - -function emptyContainer(outer, innerStr) { - return (innerStr in outer) && - (typeof outer[innerStr] === 'object') && - (Object.keys(outer[innerStr]).length === 0); -} - // convenience function to force a full redraw, mostly for use by plotly.js Plotly.redraw = function(gd) { - gd = getGraphDiv(gd); + gd = helpers.getGraphDiv(gd); if(!Lib.isPlotDiv(gd)) { throw new Error('This element is not a Plotly plot: ' + gd); @@ -844,7 +489,7 @@ Plotly.redraw = function(gd) { * @param {Object} config */ Plotly.newPlot = function(gd, data, layout, config) { - gd = getGraphDiv(gd); + gd = helpers.getGraphDiv(gd); // remove gl contexts Plots.cleanPlot([], {}, gd._fullData || {}, gd._fullLayout || {}); @@ -1198,7 +843,7 @@ function spliceTraces(gd, update, indices, maxPoints, lengthenArray, spliceArray * */ Plotly.extendTraces = function extendTraces(gd, update, indices, maxPoints) { - gd = getGraphDiv(gd); + gd = helpers.getGraphDiv(gd); var undo = spliceTraces(gd, update, indices, maxPoints, @@ -1225,7 +870,7 @@ Plotly.extendTraces = function extendTraces(gd, update, indices, maxPoints) { }; Plotly.prependTraces = function prependTraces(gd, update, indices, maxPoints) { - gd = getGraphDiv(gd); + gd = helpers.getGraphDiv(gd); var undo = spliceTraces(gd, update, indices, maxPoints, @@ -1261,7 +906,7 @@ Plotly.prependTraces = function prependTraces(gd, update, indices, maxPoints) { * */ Plotly.addTraces = function addTraces(gd, traces, newIndices) { - gd = getGraphDiv(gd); + gd = helpers.getGraphDiv(gd); var currentIndices = [], undoFunc = Plotly.deleteTraces, @@ -1278,7 +923,7 @@ Plotly.addTraces = function addTraces(gd, traces, newIndices) { if(!Array.isArray(traces)) { traces = [traces]; } - cleanData(traces, gd.data); + helpers.cleanData(traces, gd.data); // add the traces to gd.data (no redrawing yet!) for(i = 0; i < traces.length; i += 1) { @@ -1332,7 +977,7 @@ Plotly.addTraces = function addTraces(gd, traces, newIndices) { * @param {Number|Number[]} indices The indices */ Plotly.deleteTraces = function deleteTraces(gd, indices) { - gd = getGraphDiv(gd); + gd = helpers.getGraphDiv(gd); var traces = [], undoFunc = Plotly.addTraces, @@ -1398,7 +1043,7 @@ Plotly.deleteTraces = function deleteTraces(gd, indices) { * Plotly.moveTraces(gd, [b, d, e, a, c]) // same as 'move to end' */ Plotly.moveTraces = function moveTraces(gd, currentIndices, newIndices) { - gd = getGraphDiv(gd); + gd = helpers.getGraphDiv(gd); var newData = [], movingTraceMap = [], @@ -1464,40 +1109,46 @@ Plotly.moveTraces = function moveTraces(gd, currentIndices, newIndices) { return promise; }; -// ----------------------------------------------------- -// restyle and relayout: these two control all redrawing -// for data (restyle) and everything else (relayout) -// ----------------------------------------------------- - -// restyle: change styling of an existing plot -// can be called two ways: -// -// restyle(gd, astr, val [,traces]) -// gd - graph div (string id or dom element) -// astr - attribute string (like 'marker.symbol') -// val - value to give this attribute -// traces - integer or array of integers for the traces -// to alter (all if omitted) -// -// restyle(gd, aobj [,traces]) -// aobj - {astr1:val1, astr2:val2...} allows setting -// multiple attributes simultaneously -// -// 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). +/** + * 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). + */ Plotly.restyle = function restyle(gd, astr, val, traces) { - gd = getGraphDiv(gd); - clearPromiseQueue(gd); - - var i, fullLayout = gd._fullLayout, - aobj = {}; + gd = helpers.getGraphDiv(gd); + helpers.clearPromiseQueue(gd); + var aobj = {}; if(typeof astr === 'string') aobj[astr] = val; else if(Lib.isPlainObject(astr)) { + // the 3-arg form aobj = astr; - if(traces === undefined) traces = val; // the 3-arg form + if(traces === undefined) traces = val; } else { Lib.warn('Restyle fail.', astr, val, traces); @@ -1506,11 +1157,68 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { if(Object.keys(aobj).length) gd.changed = true; - if(isNumeric(traces)) traces = [traces]; - else if(!Array.isArray(traces) || !traces.length) { - traces = gd.data.map(function(v, i) { return i; }); + var specs = _restyle(gd, aobj, traces), + flags = specs.flags; + + // clear calcdata if required + if(flags.clearCalc) gd.calcdata = undefined; + + // fill in redraw sequence + var seq = []; + + if(flags.fullReplot) { + seq.push(Plotly.plot); + } + else { + seq.push(Plots.previousPromises); + + Plots.supplyDefaults(gd); + + if(flags.dostyle) seq.push(subroutines.doTraceStyle); + if(flags.docolorbars) seq.push(subroutines.doColorBars); } + 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; + }); +}; + +function _restyle(gd, aobj, _traces) { + var fullLayout = gd._fullLayout, + fullData = gd._fullData, + data = gd.data, + i; + + var traces = helpers.coerceTraceIndices(gd, _traces); + + // initialize flags + var flags = { + docalc: false, + docalcAutorange: false, + doplot: false, + dostyle: false, + docolorbars: false, + autorangeOn: false, + clearCalc: false, + fullReplot: false + }; + + // copies of the change (and previous values of anything affected) + // for the undo / redo queue + var redoit = {}, + undoit = {}, + axlist, + flagAxForDelete = {}; + // recalcAttrs attributes need a full regeneration of calcdata // as well as a replot, because the right objects may not exist, // or autorange may need recalculating @@ -1549,8 +1257,9 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { 'line.showscale', 'line.cauto', 'line.autocolorscale', 'line.reversescale', 'marker.line.showscale', 'marker.line.cauto', 'marker.line.autocolorscale', 'marker.line.reversescale' ]; + for(i = 0; i < traces.length; i++) { - if(Registry.traceIs(gd._fullData[traces[i]], 'box')) { + if(Registry.traceIs(fullData[traces[i]], 'box')) { recalcAttrs.push('name'); break; } @@ -1564,6 +1273,7 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { 'marker', 'marker.size', 'textfont', 'boxpoints', 'jitter', 'pointpos', 'whiskerwidth', 'boxmean' ]; + // replotAttrs attributes need a replot (because different // objects need to be made) but not a recalc var replotAttrs = [ @@ -1578,15 +1288,6 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { 'error_y.width', 'error_x.width', 'error_x.copy_ystyle', 'marker.maxdisplayed' ]; - // these ones show up in restyle because they make more sense - // in the style box, but they're graph-wide attributes, so set - // in gd.layout also axis scales and range show up here because - // we may need to undo them. These all trigger a recalc - // var layoutAttrs = [ - // 'barmode', 'barnorm','bargap', 'bargroupgap', - // 'boxmode', 'boxgap', 'boxgroupgap', - // '?axis.autorange', '?axis.range', '?axis.rangemode' - // ]; // these ones may alter the axis type // (at least if the first trace is involved) @@ -1594,25 +1295,16 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { 'type', 'x', 'y', 'x0', 'y0', 'orientation', 'xaxis', 'yaxis' ]; - // flags for which kind of update we need to do - var docalc = false, - docalcAutorange = false, - doplot = false, - dolayout = false, - dostyle = false, - docolorbars = false; - // copies of the change (and previous values of anything affected) - // for the undo / redo queue - var redoit = {}, - undoit = {}, - axlist, - flagAxForDelete = {}; + var zscl = ['zmin', 'zmax'], + xbins = ['xbins.start', 'xbins.end', 'xbins.size'], + ybins = ['ybins.start', 'ybins.end', 'ybins.size'], + contourAttrs = ['contours.start', 'contours.end', 'contours.size']; // At the moment, only cartesian, pie and ternary plot types can afford // to not go through a full replot var doPlotWhiteList = ['cartesian', 'pie', 'ternary']; fullLayout._basePlotModules.forEach(function(_module) { - if(doPlotWhiteList.indexOf(_module.name) === -1) docalc = true; + if(doPlotWhiteList.indexOf(_module.name) === -1) flags.docalc = true; }); // make a new empty vals array for undoit @@ -1621,9 +1313,11 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { // for autoranging multiple axes function addToAxlist(axid) { var axName = Plotly.Axes.id2name(axid); - if(axlist.indexOf(axName) === -1) { axlist.push(axName); } + if(axlist.indexOf(axName) === -1) axlist.push(axName); } + function autorangeAttr(axName) { return 'LAYOUT' + axName + '.autorange'; } + function rangeAttr(axName) { return 'LAYOUT' + axName + '.range'; } // for attrs that interact (like scales & autoscales), save the @@ -1643,7 +1337,7 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { if(attr.substr(0, 6) === 'LAYOUT') { extraparam = Lib.nestedProperty(gd.layout, attr.replace('LAYOUT', '')); } else { - extraparam = Lib.nestedProperty(gd.data[traces[i]], attr); + extraparam = Lib.nestedProperty(data[traces[i]], attr); } if(!(attr in undoit)) { @@ -1656,10 +1350,6 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { extraparam.set(val); } } - var zscl = ['zmin', 'zmax'], - xbins = ['xbins.start', 'xbins.end', 'xbins.size'], - ybins = ['ybins.start', 'ybins.end', 'ybins.size'], - contourAttrs = ['contours.start', 'contours.end', 'contours.size']; // now make the changes to gd.data (and occasionally gd.layout) // and figure out what kind of graphics update we need to do @@ -1670,6 +1360,7 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { param, oldVal, newVal; + redoit[ai] = vi; if(ai.substr(0, 6) === 'LAYOUT') { @@ -1680,18 +1371,18 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { param.set(Array.isArray(vi) ? vi[0] : vi); // ironically, the layout attrs in restyle only require replot, // not relayout - docalc = true; + flags.docalc = true; continue; } // take no chances on transforms - if(ai.substr(0, 10) === 'transforms') docalc = true; + if(ai.substr(0, 10) === 'transforms') flags.docalc = true; // set attribute in gd.data undoit[ai] = a0(); for(i = 0; i < traces.length; i++) { - cont = gd.data[traces[i]]; - contFull = gd._fullData[traces[i]]; + cont = data[traces[i]]; + contFull = fullData[traces[i]]; param = Lib.nestedProperty(cont, ai); oldVal = param.get(); newVal = Array.isArray(vi) ? vi[i % vi.length] : vi; @@ -1824,7 +1515,7 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { cont.orientation = {v: 'h', h: 'v'}[contFull.orientation]; } - swapXYData(cont); + helpers.swapXYData(cont); } // all the other ones, just modify that one attribute else param.set(newVal); @@ -1849,7 +1540,7 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { // check if we need to call axis type if((traces.indexOf(0) !== -1) && (axtypeAttrs.indexOf(ai) !== -1)) { Plotly.Axes.clearTypes(gd, traces); - docalc = true; + flags.docalc = true; } // switching from auto to manual binning or z scaling doesn't @@ -1857,11 +1548,11 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { // box. everything else at least needs to apply styles if((['autobinx', 'autobiny', 'zauto'].indexOf(ai) === -1) || newVal !== false) { - dostyle = true; + flags.dostyle = true; } if(['colorbar', 'line'].indexOf(param.parts[0]) !== -1 || param.parts[0] === 'marker' && param.parts[1] === 'colorbar') { - docolorbars = true; + flags.docolorbars = true; } if(recalcAttrs.indexOf(ai) !== -1) { @@ -1870,13 +1561,13 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { if(['orientation', 'type'].indexOf(ai) !== -1) { axlist = []; for(i = 0; i < traces.length; i++) { - var trace = gd.data[traces[i]]; + var trace = data[traces[i]]; if(Registry.traceIs(trace, 'cartesian')) { addToAxlist(trace.xaxis || 'x'); addToAxlist(trace.yaxis || 'y'); - if(astr === 'type') { + if(ai === 'type') { doextra(['autobinx', 'autobiny'], true, i); } } @@ -1885,12 +1576,17 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { doextra(axlist.map(autorangeAttr), true, 0); doextra(axlist.map(rangeAttr), [0, 1], 0); } - docalc = true; + flags.docalc = true; } - else if(replotAttrs.indexOf(ai) !== -1) doplot = true; - else if(autorangeAttrs.indexOf(ai) !== -1) docalcAutorange = true; + else if(replotAttrs.indexOf(ai) !== -1) flags.doplot = true; + else if(autorangeAttrs.indexOf(ai) !== -1) flags.docalcAutorange = true; } + // do we need to force a recalc? + Plotly.Axes.list(gd).forEach(function(ax) { + if(ax.autorange) flags.autorangeOn = true; + }); + // check axes we've flagged for possible deletion // flagAxForDelete is a hash so we can make sure we only get each axis once var axListForDelete = Object.keys(flagAxForDelete); @@ -1899,9 +1595,10 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { var axId = axListForDelete[i], axLetter = axId.charAt(0), axAttr = axLetter + 'axis'; - for(var j = 0; j < gd.data.length; j++) { - if(Registry.traceIs(gd.data[j], 'cartesian') && - (gd.data[j][axAttr] || axLetter) === axId) { + + for(var j = 0; j < data.length; j++) { + if(Registry.traceIs(data[j], 'cartesian') && + (data[j][axAttr] || axLetter) === axId) { continue axisLoop; } } @@ -1910,148 +1607,52 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { doextra('LAYOUT' + Plotly.Axes.id2name(axId), null, 0); } - // now all attribute mods are done, as are redo and undo - // so we can save them - Queue.add(gd, restyle, [gd, undoit, traces], restyle, [gd, redoit, traces]); - - // do we need to force a recalc? - var autorangeOn = false; - Plotly.Axes.list(gd).forEach(function(ax) { - if(ax.autorange) autorangeOn = true; - }); - if(docalc || dolayout || (docalcAutorange && autorangeOn)) { - gd.calcdata = undefined; - } - - // now update the graphics - // a complete layout redraw takes care of plot and - var seq; - if(dolayout) { - seq = [function changeLayout() { - var copyLayout = gd.layout; - gd.layout = undefined; - return Plotly.plot(gd, '', copyLayout); - }]; + // combine a few flags together; + if(flags.docalc || (flags.docalcAutorange && flags.autorangeOn)) { + flags.clearCalc = true; } - else if(docalc || doplot || docalcAutorange) { - seq = [Plotly.plot]; + if(flags.docalc || flags.doplot || flags.docalcAutorange) { + flags.fullReplot = true; } - else { - Plots.supplyDefaults(gd); - seq = [Plots.previousPromises]; - if(dostyle) { - seq.push(function doStyle() { - // first see if we need to do arraysToCalcdata - // call it regardless of what change we made, in case - // supplyDefaults brought in an array that was already - // in gd.data but not in gd._fullData previously - var i, cdi, arraysToCalcdata; - for(i = 0; i < gd.calcdata.length; i++) { - cdi = gd.calcdata[i]; - arraysToCalcdata = (((cdi[0] || {}).trace || {})._module || {}).arraysToCalcdata; - if(arraysToCalcdata) arraysToCalcdata(cdi); - } - - Plots.style(gd); - Registry.getComponentMethod('legend', 'draw')(gd); - return Plots.previousPromises(gd); - }); - } - if(docolorbars) { - seq.push(function doColorBars() { - gd.calcdata.forEach(function(cd) { - if((cd[0].t || {}).cb) { - var trace = cd[0].trace, - cb = cd[0].t.cb; - - if(Registry.traceIs(trace, 'contour')) { - cb.line({ - width: trace.contours.showlines !== false ? - trace.line.width : 0, - dash: trace.line.dash, - color: trace.contours.coloring === 'line' ? - cb._opts.line.color : trace.line.color - }); - } - if(Registry.traceIs(trace, 'markerColorscale')) { - cb.options(trace.marker.colorbar)(); - } - else cb.options(trace.colorbar)(); - } - }); - return Plots.previousPromises(gd); - }); - } - } - - var plotDone = Lib.syncOrAsync(seq, gd); - - if(!plotDone || !plotDone.then) plotDone = Promise.resolve(); - - return plotDone.then(function() { - gd.emit('plotly_restyle', Lib.extendDeepNoArrays([], [redoit, traces])); - return gd; - }); -}; - -// swap all the data and data attributes associated with x and y -function swapXYData(trace) { - var i; - Lib.swapAttrs(trace, ['?', '?0', 'd?', '?bins', 'nbins?', 'autobin?', '?src', 'error_?']); - if(Array.isArray(trace.z) && Array.isArray(trace.z[0])) { - if(trace.transpose) delete trace.transpose; - else trace.transpose = true; - } - if(trace.error_x && trace.error_y) { - var errorY = trace.error_y, - copyYstyle = ('copy_ystyle' in errorY) ? errorY.copy_ystyle : - !(errorY.color || errorY.thickness || errorY.width); - Lib.swapAttrs(trace, ['error_?.copy_ystyle']); - if(copyYstyle) { - Lib.swapAttrs(trace, ['error_?.color', 'error_?.thickness', 'error_?.width']); - } - } - if(trace.hoverinfo) { - var hoverInfoParts = trace.hoverinfo.split('+'); - for(i = 0; i < hoverInfoParts.length; i++) { - if(hoverInfoParts[i] === 'x') hoverInfoParts[i] = 'y'; - else if(hoverInfoParts[i] === 'y') hoverInfoParts[i] = 'x'; - } - trace.hoverinfo = hoverInfoParts.join('+'); - } + return { + flags: flags, + undoit: undoit, + redoit: redoit, + traces: traces, + eventData: Lib.extendDeepNoArrays([], [redoit, traces]) + }; } -// relayout: change layout in an existing plot -// can be called two ways: -// -// relayout(gd, astr, val) -// gd - graph div (string id or dom element) -// astr - attribute string (like 'xaxis.range[0]') -// val - value to give this attribute -// -// relayout(gd,aobj) -// aobj - {astr1:val1, astr2:val2...} -// allows setting multiple attributes simultaneously +/** + * relayout: update layout 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 `'xaxis.range[0]'`) to update + * @param {*} val + * value to give this attribute + * + * Signature 2: + * @param {String | HTMLDivElement} gd + * (as in signature 1) + * @param {Object} aobj + * attribute object `{astr1: val1, astr2: val2 ...}` + * allows setting multiple attributes simultaneously + */ Plotly.relayout = function relayout(gd, astr, val) { - gd = getGraphDiv(gd); - clearPromiseQueue(gd); + gd = helpers.getGraphDiv(gd); + helpers.clearPromiseQueue(gd); if(gd.framework && gd.framework.isPolar) { return Promise.resolve(gd); } - var layout = gd.layout, - fullLayout = gd._fullLayout, - aobj = {}, - dolegend = false, - doticks = false, - dolayoutstyle = false, - doplot = false, - docalc = false, - domodebar = false, - newkey, axes, keys, xyref, scene, axisAttr, i; - + var aobj = {}; if(typeof astr === 'string') aobj[astr] = val; else if(Lib.isPlainObject(astr)) aobj = astr; else { @@ -2061,36 +1662,84 @@ Plotly.relayout = function relayout(gd, astr, val) { if(Object.keys(aobj).length) gd.changed = true; - keys = Object.keys(aobj); - axes = Plotly.Axes.list(gd); + var specs = _relayout(gd, aobj), + flags = specs.flags; + + // clear calcdata if required + if(flags.docalc) gd.calcdata = undefined; + + // fill in redraw sequence + var seq = []; + + if(flags.layoutReplot) { + seq.push(subroutines.layoutReplot); + } + else if(Object.keys(aobj).length) { + seq.push(Plots.previousPromises); + Plots.supplyDefaults(gd); + + if(flags.dolegend) seq.push(subroutines.doLegend); + if(flags.dolayoutstyle) seq.push(subroutines.layoutStyles); + if(flags.doticks) seq.push(subroutines.doTicksRelayout); + if(flags.domodebar) seq.push(subroutines.doModeBar); + } + + Queue.add(gd, + relayout, [gd, specs.undoit], + relayout, [gd, specs.redoit] + ); + + var plotDone = Lib.syncOrAsync(seq, gd); + if(!plotDone || !plotDone.then) plotDone = Promise.resolve(gd); + + return plotDone.then(function() { + subroutines.setRangeSliderRange(gd, specs.eventData); + gd.emit('plotly_relayout', specs.eventData); + return gd; + }); +}; + +function _relayout(gd, aobj) { + var layout = gd.layout, + fullLayout = gd._fullLayout, + keys = Object.keys(aobj), + axes = Plotly.Axes.list(gd), + i; + // look for 'allaxes', split out into all axes + // in case of 3D the axis are nested within a scene which is held in _id for(i = 0; i < keys.length; i++) { - // look for 'allaxes', split out into all axes if(keys[i].indexOf('allaxes') === 0) { for(var j = 0; j < axes.length; j++) { - // in case of 3D the axis are nested within a scene which is held in _id - scene = axes[j]._id.substr(1); - axisAttr = (scene.indexOf('scene') !== -1) ? (scene + '.') : ''; - newkey = keys[i].replace('allaxes', axisAttr + axes[j]._name); - if(!aobj[newkey]) { aobj[newkey] = aobj[keys[i]]; } + var scene = axes[j]._id.substr(1), + axisAttr = (scene.indexOf('scene') !== -1) ? (scene + '.') : '', + newkey = keys[i].replace('allaxes', axisAttr + axes[j]._name); + + if(!aobj[newkey]) aobj[newkey] = aobj[keys[i]]; } - delete aobj[keys[i]]; - } - // split annotation.ref into xref and yref - if(keys[i].match(/^annotations\[[0-9-]+\].ref$/)) { - xyref = aobj[keys[i]].split('y'); - aobj[keys[i].replace('ref', 'xref')] = xyref[0]; - aobj[keys[i].replace('ref', 'yref')] = xyref.length === 2 ? - ('y' + xyref[1]) : 'paper'; + delete aobj[keys[i]]; } } + // initialize flags + var flags = { + dolegend: false, + doticks: false, + dolayoutstyle: false, + doplot: false, + docalc: false, + domodebar: false, + layoutReplot: false + }; + // copies of the change (and previous values of anything affected) // for the undo / redo queue var redoit = {}, undoit = {}; + var hw = ['height', 'width']; + // for attrs that interact (like scales & autoscales), save the // old vals before making the change // val=undefined will not set a value, just record what the value was. @@ -2114,8 +1763,6 @@ Plotly.relayout = function relayout(gd, astr, val) { return (fullLayout[axName] || {}).autorange; } - var hw = ['height', 'width']; - // alter gd.layout for(var ai in aobj) { var p = Lib.nestedProperty(layout, ai), @@ -2168,23 +1815,24 @@ Plotly.relayout = function relayout(gd, astr, val) { doextra([ptrunk + '.tick0', ptrunk + '.dtick'], undefined); } else if(/[xy]axis[0-9]*?$/.test(pleaf) && !Object.keys(vi || {}).length) { - docalc = true; + flags.docalc = true; } else if(/[xy]axis[0-9]*\.categoryorder$/.test(pleafPlus)) { - docalc = true; + flags.docalc = true; } else if(/[xy]axis[0-9]*\.categoryarray/.test(pleafPlus)) { - docalc = true; + flags.docalc = true; } if(pleafPlus.indexOf('rangeslider') !== -1) { - docalc = true; + flags.docalc = true; } // toggling log without autorange: need to also recalculate ranges // logical XOR (ie are we toggling log) if(pleaf === 'type' && ((parentFull.type === 'log') !== (vi === 'log'))) { var ax = parentIn; + if(!ax || !ax.range) { doextra(ptrunk + '.autorange', true); } @@ -2223,8 +1871,8 @@ Plotly.relayout = function relayout(gd, astr, val) { parentIn.range = [1, 0]; } - if(parentFull.autorange) docalc = true; - else doplot = true; + if(parentFull.autorange) flags.docalc = true; + else flags.doplot = true; } // send annotation and shape mods one-by-one through Annotations.draw(), // don't set via nestedProperty @@ -2254,7 +1902,7 @@ Plotly.relayout = function relayout(gd, astr, val) { if((refAutorange(obji, 'x') || refAutorange(obji, 'y')) && !Lib.containsAny(ai, ['color', 'opacity', 'align', 'dash'])) { - docalc = true; + flags.docalc = true; } // TODO: combine all edits to a given annotation / shape into one call @@ -2282,7 +1930,7 @@ Plotly.relayout = function relayout(gd, astr, val) { for(i = 0; i < diff; i++) fullLayers.push({}); - doplot = true; + flags.doplot = true; } else if(p.parts[0] === 'updatemenus') { Lib.extendDeepAll(gd.layout, Lib.objectFromPath(ai, vi)); @@ -2291,38 +1939,38 @@ Plotly.relayout = function relayout(gd, astr, val) { diff = (p.parts[2] + 1) - menus.length; for(i = 0; i < diff; i++) menus.push({}); - doplot = true; + flags.doplot = true; } // alter gd.layout else { // check whether we can short-circuit a full redraw // 3d or geo at this point just needs to redraw. - if(p.parts[0].indexOf('scene') === 0) doplot = true; - else if(p.parts[0].indexOf('geo') === 0) doplot = true; - else if(p.parts[0].indexOf('ternary') === 0) doplot = true; + if(p.parts[0].indexOf('scene') === 0) flags.doplot = true; + else if(p.parts[0].indexOf('geo') === 0) flags.doplot = true; + else if(p.parts[0].indexOf('ternary') === 0) flags.doplot = true; else if(fullLayout._has('gl2d') && (ai.indexOf('axis') !== -1 || p.parts[0] === 'plot_bgcolor') - ) doplot = true; - else if(ai === 'hiddenlabels') docalc = true; - else if(p.parts[0].indexOf('legend') !== -1) dolegend = true; - else if(ai.indexOf('title') !== -1) doticks = true; - else if(p.parts[0].indexOf('bgcolor') !== -1) dolayoutstyle = true; + ) flags.doplot = true; + else if(ai === 'hiddenlabels') flags.docalc = true; + else if(p.parts[0].indexOf('legend') !== -1) flags.dolegend = true; + else if(ai.indexOf('title') !== -1) flags.doticks = true; + else if(p.parts[0].indexOf('bgcolor') !== -1) flags.dolayoutstyle = true; else if(p.parts.length > 1 && Lib.containsAny(p.parts[1], ['tick', 'exponent', 'grid', 'zeroline'])) { - doticks = true; + flags.doticks = true; } else if(ai.indexOf('.linewidth') !== -1 && ai.indexOf('axis') !== -1) { - doticks = dolayoutstyle = true; + flags.doticks = flags.dolayoutstyle = true; } else if(p.parts.length > 1 && p.parts[1].indexOf('line') !== -1) { - dolayoutstyle = true; + flags.dolayoutstyle = true; } else if(p.parts.length > 1 && p.parts[1] === 'mirror') { - doticks = dolayoutstyle = true; + flags.doticks = flags.dolayoutstyle = true; } else if(ai === 'margin.pad') { - doticks = dolayoutstyle = true; + flags.doticks = flags.dolayoutstyle = true; } else if(p.parts[0] === 'margin' || p.parts[1] === 'autorange' || @@ -2330,126 +1978,123 @@ Plotly.relayout = function relayout(gd, astr, val) { p.parts[1] === 'type' || p.parts[1] === 'domain' || ai.match(/^(bar|box|font)/)) { - docalc = true; + flags.docalc = true; } /* * hovermode and dragmode don't need any redrawing, since they just - * affect reaction to user input. everything else, assume full replot. + * affect reaction to user input, everything else, assume full replot. * height, width, autosize get dealt with below. Except for the case of * of subplots - scenes - which require scene.updateFx to be called. */ - else if(['hovermode', 'dragmode'].indexOf(ai) !== -1) domodebar = true; + else if(['hovermode', 'dragmode'].indexOf(ai) !== -1) flags.domodebar = true; else if(['hovermode', 'dragmode', 'height', 'width', 'autosize'].indexOf(ai) === -1) { - doplot = true; + flags.doplot = true; } p.set(vi); } } - // now all attribute mods are done, as are - // redo and undo so we can save them - Queue.add(gd, relayout, [gd, undoit], relayout, [gd, redoit]); // calculate autosizing - if size hasn't changed, // will remove h&w so we don't need to redraw if(aobj.autosize) aobj = plotAutoSize(gd, aobj); + if(aobj.height || aobj.width || aobj.autosize) flags.docalc = true; - if(aobj.height || aobj.width || aobj.autosize) docalc = true; + if(flags.doplot || flags.docalc) { + flags.layoutReplot = true; + } - // redraw - // first check if there's still anything to do - var ak = Object.keys(aobj), - seq = [Plots.previousPromises]; + // now all attribute mods are done, as are + // redo and undo so we can save them - if(doplot || docalc) { - seq.push(function layoutReplot() { - // force plot() to redo the layout - gd.layout = undefined; + return { + flags: flags, + undoit: undoit, + redoit: redoit, + eventData: Lib.extendDeep({}, redoit) + }; +} - // force it to redo calcdata? - if(docalc) gd.calcdata = undefined; +/** + * update: update trace and layout attributes of an existing plot + * + * @param {String | HTMLDivElement} gd + * the id or DOM element of the graph container div + * @param {Object} traceUpdate + * attribute object `{astr1: val1, astr2: val2 ...}` + * corresponding to updates in the plot's traces + * @param {Object} layoutUpdate + * attribute object `{astr1: val1, astr2: val2 ...}` + * corresponding to updates in the plot's layout + * @param {Number[] | Number} [traces] + * integer or array of integers for the traces to alter (all if omitted) + * + */ +Plotly.update = function update(gd, traceUpdate, layoutUpdate, traces) { + gd = helpers.getGraphDiv(gd); + helpers.clearPromiseQueue(gd); - // replot with the modified layout - return Plotly.plot(gd, '', layout); - }); + if(gd.framework && gd.framework.isPolar) { + return Promise.resolve(gd); } - else if(ak.length) { - // if we didn't need to redraw entirely, just do the needed parts - Plots.supplyDefaults(gd); - fullLayout = gd._fullLayout; - if(dolegend) { - seq.push(function doLegend() { - Registry.getComponentMethod('legend', 'draw')(gd); - return Plots.previousPromises(gd); - }); - } + if(!Lib.isPlainObject(traceUpdate)) traceUpdate = {}; + if(!Lib.isPlainObject(layoutUpdate)) layoutUpdate = {}; - if(dolayoutstyle) seq.push(layoutStyles); + if(Object.keys(traceUpdate).length) gd.changed = true; + if(Object.keys(layoutUpdate).length) gd.changed = true; - if(doticks) { - seq.push(function() { - Plotly.Axes.doTicks(gd, 'redraw'); - drawMainTitle(gd); - return Plots.previousPromises(gd); - }); - } + var restyleSpecs = _restyle(gd, traceUpdate, traces), + restyleFlags = restyleSpecs.flags; - // this is decoupled enough it doesn't need async regardless - if(domodebar) { - var subplotIds; - ModeBar.manage(gd); + var relayoutSpecs = _relayout(gd, layoutUpdate), + relayoutFlags = relayoutSpecs.flags; - Plotly.Fx.supplyLayoutDefaults(gd.layout, fullLayout, gd._fullData); - Plotly.Fx.init(gd); + // clear calcdata if required + if(restyleFlags.clearCalc || relayoutFlags.docalc) gd.calcdata = undefined; - subplotIds = Plots.getSubplotIds(fullLayout, 'gl3d'); - for(i = 0; i < subplotIds.length; i++) { - scene = fullLayout[subplotIds[i]]._scene; - scene.updateFx(fullLayout.dragmode, fullLayout.hovermode); - } - - subplotIds = Plots.getSubplotIds(fullLayout, 'gl2d'); - for(i = 0; i < subplotIds.length; i++) { - scene = fullLayout._plots[subplotIds[i]]._scene2d; - scene.updateFx(fullLayout); - } + // fill in redraw sequence + var seq = []; - subplotIds = Plots.getSubplotIds(fullLayout, 'geo'); - for(i = 0; i < subplotIds.length; i++) { - var geo = fullLayout[subplotIds[i]]._geo; - geo.updateFx(fullLayout.hovermode); - } - } + if(restyleFlags.fullReplot && relayoutFlags.layoutReplot) { + var layout = gd.layout; + gd.layout = undefined; + seq.push(function() { return Plotly.plot(gd, gd.data, layout); }); } + else if(restyleFlags.fullReplot) { + seq.push(Plotly.plot); + } + else if(relayoutFlags.layoutReplot) { + seq.push(subroutines.layoutReplot); + } + else { + seq.push(Plots.previousPromises); + Plots.supplyDefaults(gd); - function setRange(changes) { - - var newMin = changes['xaxis.range'] ? changes['xaxis.range'][0] : changes['xaxis.range[0]'], - newMax = changes['xaxis.range'] ? changes['xaxis.range'][1] : changes['xaxis.range[1]']; - - var rangeSlider = fullLayout.xaxis && fullLayout.xaxis.rangeslider ? - fullLayout.xaxis.rangeslider : {}; - - if(rangeSlider.visible) { - if(newMin || newMax) { - fullLayout.xaxis.rangeslider.setRange(newMin, newMax); - } else if(changes['xaxis.autorange']) { - fullLayout.xaxis.rangeslider.setRange(); - } - } + if(restyleFlags.dostyle) seq.push(subroutines.doTraceStyle); + if(restyleFlags.docolorbars) seq.push(subroutines.doColorBars); + if(relayoutFlags.dolegend) seq.push(subroutines.doLegend); + if(relayoutFlags.dolayoutstyle) seq.push(subroutines.layoutStyles); + if(relayoutFlags.doticks) seq.push(subroutines.doTicksRelayout); + if(relayoutFlags.domodebar) seq.push(subroutines.doModeBar); } - var plotDone = Lib.syncOrAsync(seq, gd); + Queue.add(gd, + update, [gd, restyleSpecs.undoit, relayoutSpecs.undoit, restyleSpecs.traces], + update, [gd, restyleSpecs.redoit, relayoutSpecs.redoit, restyleSpecs.traces] + ); + var plotDone = Lib.syncOrAsync(seq, gd); if(!plotDone || !plotDone.then) plotDone = Promise.resolve(gd); return plotDone.then(function() { - var changes = Lib.extendDeep({}, redoit); + subroutines.setRangeSliderRange(gd, relayoutSpecs.eventData); - setRange(changes); - gd.emit('plotly_relayout', changes); + gd.emit('plotly_update', { + data: restyleSpecs.eventData, + layout: relayoutSpecs.eventData + }); return gd; }); @@ -2483,7 +2128,7 @@ Plotly.relayout = function relayout(gd, astr, val) { * configuration for the animation */ Plotly.animate = function(gd, frameOrGroupNameOrFrameList, animationOpts) { - gd = getGraphDiv(gd); + gd = helpers.getGraphDiv(gd); if(!Lib.isPlotDiv(gd)) { throw new Error('This element is not a Plotly plot: ' + gd); @@ -2637,7 +2282,7 @@ Plotly.animate = function(gd, frameOrGroupNameOrFrameList, animationOpts) { Plots.transition(gd, newFrame.frame.data, newFrame.frame.layout, - newFrame.frame.traces, + helpers.coerceTraceIndices(gd, newFrame.frame.traces), newFrame.frameOpts, newFrame.transitionOpts ); @@ -2766,7 +2411,7 @@ Plotly.animate = function(gd, frameOrGroupNameOrFrameList, animationOpts) { * will be overwritten. */ Plotly.addFrames = function(gd, frameList, indices) { - gd = getGraphDiv(gd); + gd = helpers.getGraphDiv(gd); if(!Lib.isPlotDiv(gd)) { throw new Error('This element is not a Plotly plot: ' + gd); @@ -2853,7 +2498,7 @@ Plotly.addFrames = function(gd, frameList, indices) { * list of integer indices of frames to be deleted */ Plotly.deleteFrames = function(gd, frameList) { - gd = getGraphDiv(gd); + gd = helpers.getGraphDiv(gd); if(!Lib.isPlotDiv(gd)) { throw new Error('This element is not a Plotly plot: ' + gd); @@ -2890,7 +2535,7 @@ Plotly.deleteFrames = function(gd, frameList) { * the id or DOM element of the graph container div */ Plotly.purge = function purge(gd) { - gd = getGraphDiv(gd); + gd = helpers.getGraphDiv(gd); var fullLayout = gd._fullLayout || {}, fullData = gd._fullData || []; @@ -3116,7 +2761,7 @@ function makePlotFramework(gd) { // position and style the containers, make main title var frameWorkDone = Lib.syncOrAsync([ - layoutStyles, + subroutines.layoutStyles, function goAxes() { return Plotly.Axes.doTicks(gd, 'redraw'); }, Fx.init ], gd); @@ -3286,212 +2931,3 @@ function makeCartesianPlotFramwork(gd, subplots) { .classed('crisp', true); }); } - -// layoutStyles: styling for plot layout elements -function layoutStyles(gd) { - return Lib.syncOrAsync([Plots.doAutoMargin, lsInner], gd); -} - -function lsInner(gd) { - var fullLayout = gd._fullLayout, - gs = fullLayout._size, - axList = Plotly.Axes.list(gd), - i; - - // clear axis line positions, to be set in the subplot loop below - for(i = 0; i < axList.length; i++) axList[i]._linepositions = {}; - - fullLayout._paperdiv - .style({ - width: fullLayout.width + 'px', - height: fullLayout.height + 'px' - }) - .selectAll('.main-svg') - .call(Drawing.setSize, fullLayout.width, fullLayout.height); - - gd._context.setBackground(gd, fullLayout.paper_bgcolor); - - var freefinished = []; - fullLayout._paper.selectAll('g.subplot').each(function(subplot) { - var plotinfo = fullLayout._plots[subplot], - xa = Plotly.Axes.getFromId(gd, subplot, 'x'), - ya = Plotly.Axes.getFromId(gd, subplot, 'y'); - xa.setScale(); // this may already be done... not sure - ya.setScale(); - - if(plotinfo.bg) { - plotinfo.bg - .call(Drawing.setRect, - xa._offset - gs.p, ya._offset - gs.p, - xa._length + 2 * gs.p, ya._length + 2 * gs.p) - .call(Color.fill, fullLayout.plot_bgcolor); - } - - - // Clip so that data only shows up on the plot area. - plotinfo.clipId = 'clip' + fullLayout._uid + subplot + 'plot'; - - var plotClip = fullLayout._defs.selectAll('g.clips') - .selectAll('#' + plotinfo.clipId) - .data([0]); - - plotClip.enter().append('clipPath') - .attr({ - 'class': 'plotclip', - 'id': plotinfo.clipId - }) - .append('rect'); - - plotClip.selectAll('rect') - .attr({ - 'width': xa._length, - 'height': ya._length - }); - - - plotinfo.plot.call(Lib.setTranslate, xa._offset, ya._offset); - plotinfo.plot.call(Drawing.setClipUrl, plotinfo.clipId); - - var xlw = Drawing.crispRound(gd, xa.linewidth, 1), - ylw = Drawing.crispRound(gd, ya.linewidth, 1), - xp = gs.p + ylw, - xpathPrefix = 'M' + (-xp) + ',', - xpathSuffix = 'h' + (xa._length + 2 * xp), - showfreex = xa.anchor === 'free' && - freefinished.indexOf(xa._id) === -1, - freeposx = gs.h * (1 - (xa.position||0)) + ((xlw / 2) % 1), - showbottom = - (xa.anchor === ya._id && (xa.mirror || xa.side !== 'top')) || - xa.mirror === 'all' || xa.mirror === 'allticks' || - (xa.mirrors && xa.mirrors[ya._id + 'bottom']), - bottompos = ya._length + gs.p + xlw / 2, - showtop = - (xa.anchor === ya._id && (xa.mirror || xa.side === 'top')) || - xa.mirror === 'all' || xa.mirror === 'allticks' || - (xa.mirrors && xa.mirrors[ya._id + 'top']), - toppos = -gs.p - xlw / 2, - - // shorten y axis lines so they don't overlap x axis lines - yp = gs.p, - // except where there's no x line - // TODO: this gets more complicated with multiple x and y axes - ypbottom = showbottom ? 0 : xlw, - yptop = showtop ? 0 : xlw, - ypathSuffix = ',' + (-yp - yptop) + - 'v' + (ya._length + 2 * yp + yptop + ypbottom), - showfreey = ya.anchor === 'free' && - freefinished.indexOf(ya._id) === -1, - freeposy = gs.w * (ya.position||0) + ((ylw / 2) % 1), - showleft = - (ya.anchor === xa._id && (ya.mirror || ya.side !== 'right')) || - ya.mirror === 'all' || ya.mirror === 'allticks' || - (ya.mirrors && ya.mirrors[xa._id + 'left']), - leftpos = -gs.p - ylw / 2, - showright = - (ya.anchor === xa._id && (ya.mirror || ya.side === 'right')) || - ya.mirror === 'all' || ya.mirror === 'allticks' || - (ya.mirrors && ya.mirrors[xa._id + 'right']), - rightpos = xa._length + gs.p + ylw / 2; - - // save axis line positions for ticks, draggers, etc to reference - // each subplot gets an entry: - // [left or bottom, right or top, free, main] - // main is the position at which to draw labels and draggers, if any - xa._linepositions[subplot] = [ - showbottom ? bottompos : undefined, - showtop ? toppos : undefined, - showfreex ? freeposx : undefined - ]; - if(xa.anchor === ya._id) { - xa._linepositions[subplot][3] = xa.side === 'top' ? - toppos : bottompos; - } - else if(showfreex) { - xa._linepositions[subplot][3] = freeposx; - } - - ya._linepositions[subplot] = [ - showleft ? leftpos : undefined, - showright ? rightpos : undefined, - showfreey ? freeposy : undefined - ]; - if(ya.anchor === xa._id) { - ya._linepositions[subplot][3] = ya.side === 'right' ? - rightpos : leftpos; - } - else if(showfreey) { - ya._linepositions[subplot][3] = freeposy; - } - - // translate all the extra stuff to have the - // same origin as the plot area or axes - var origin = 'translate(' + xa._offset + ',' + ya._offset + ')', - originx = origin, - originy = origin; - if(showfreex) { - originx = 'translate(' + xa._offset + ',' + gs.t + ')'; - toppos += ya._offset - gs.t; - bottompos += ya._offset - gs.t; - } - if(showfreey) { - originy = 'translate(' + gs.l + ',' + ya._offset + ')'; - leftpos += xa._offset - gs.l; - rightpos += xa._offset - gs.l; - } - - plotinfo.xlines - .attr('transform', originx) - .attr('d', ( - (showbottom ? (xpathPrefix + bottompos + xpathSuffix) : '') + - (showtop ? (xpathPrefix + toppos + xpathSuffix) : '') + - (showfreex ? (xpathPrefix + freeposx + xpathSuffix) : '')) || - // so it doesn't barf with no lines shown - 'M0,0') - .style('stroke-width', xlw + 'px') - .call(Color.stroke, xa.showline ? - xa.linecolor : 'rgba(0,0,0,0)'); - plotinfo.ylines - .attr('transform', originy) - .attr('d', ( - (showleft ? ('M' + leftpos + ypathSuffix) : '') + - (showright ? ('M' + rightpos + ypathSuffix) : '') + - (showfreey ? ('M' + freeposy + ypathSuffix) : '')) || - 'M0,0') - .attr('stroke-width', ylw + 'px') - .call(Color.stroke, ya.showline ? - ya.linecolor : 'rgba(0,0,0,0)'); - - plotinfo.xaxislayer.attr('transform', originx); - plotinfo.yaxislayer.attr('transform', originy); - plotinfo.gridlayer.attr('transform', origin); - plotinfo.zerolinelayer.attr('transform', origin); - plotinfo.draglayer.attr('transform', origin); - - // mark free axes as displayed, so we don't draw them again - if(showfreex) { freefinished.push(xa._id); } - if(showfreey) { freefinished.push(ya._id); } - }); - - Plotly.Axes.makeClipPaths(gd); - - drawMainTitle(gd); - - ModeBar.manage(gd); - - return gd._promises.length && Promise.all(gd._promises); -} - -function drawMainTitle(gd) { - var fullLayout = gd._fullLayout; - - Titles.draw(gd, 'gtitle', { - propContainer: fullLayout, - propName: 'title', - dfltName: 'Plot', - attributes: { - x: fullLayout.width / 2, - y: fullLayout._size.t / 2, - 'text-anchor': 'middle' - } - }); -} diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js new file mode 100644 index 00000000000..758a778df92 --- /dev/null +++ b/src/plot_api/subroutines.js @@ -0,0 +1,339 @@ +/** +* Copyright 2012-2016, 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 Plotly = require('../plotly'); +var Registry = require('../registry'); +var Plots = require('../plots/plots'); +var Lib = require('../lib'); + +var Color = require('../components/color'); +var Drawing = require('../components/drawing'); +var Titles = require('../components/titles'); +var ModeBar = require('../components/modebar'); + + +exports.layoutStyles = function(gd) { + return Lib.syncOrAsync([Plots.doAutoMargin, exports.lsInner], gd); +}; + +exports.lsInner = function(gd) { + var fullLayout = gd._fullLayout, + gs = fullLayout._size, + axList = Plotly.Axes.list(gd), + i; + + // clear axis line positions, to be set in the subplot loop below + for(i = 0; i < axList.length; i++) axList[i]._linepositions = {}; + + fullLayout._paperdiv + .style({ + width: fullLayout.width + 'px', + height: fullLayout.height + 'px' + }) + .selectAll('.main-svg') + .call(Drawing.setSize, fullLayout.width, fullLayout.height); + + gd._context.setBackground(gd, fullLayout.paper_bgcolor); + + var freefinished = []; + fullLayout._paper.selectAll('g.subplot').each(function(subplot) { + var plotinfo = fullLayout._plots[subplot], + xa = Plotly.Axes.getFromId(gd, subplot, 'x'), + ya = Plotly.Axes.getFromId(gd, subplot, 'y'); + xa.setScale(); // this may already be done... not sure + ya.setScale(); + + if(plotinfo.bg) { + plotinfo.bg + .call(Drawing.setRect, + xa._offset - gs.p, ya._offset - gs.p, + xa._length + 2 * gs.p, ya._length + 2 * gs.p) + .call(Color.fill, fullLayout.plot_bgcolor); + } + + + // Clip so that data only shows up on the plot area. + plotinfo.clipId = 'clip' + fullLayout._uid + subplot + 'plot'; + + var plotClip = fullLayout._defs.selectAll('g.clips') + .selectAll('#' + plotinfo.clipId) + .data([0]); + + plotClip.enter().append('clipPath') + .attr({ + 'class': 'plotclip', + 'id': plotinfo.clipId + }) + .append('rect'); + + plotClip.selectAll('rect') + .attr({ + 'width': xa._length, + 'height': ya._length + }); + + + plotinfo.plot.call(Lib.setTranslate, xa._offset, ya._offset); + plotinfo.plot.call(Drawing.setClipUrl, plotinfo.clipId); + + var xlw = Drawing.crispRound(gd, xa.linewidth, 1), + ylw = Drawing.crispRound(gd, ya.linewidth, 1), + xp = gs.p + ylw, + xpathPrefix = 'M' + (-xp) + ',', + xpathSuffix = 'h' + (xa._length + 2 * xp), + showfreex = xa.anchor === 'free' && + freefinished.indexOf(xa._id) === -1, + freeposx = gs.h * (1 - (xa.position||0)) + ((xlw / 2) % 1), + showbottom = + (xa.anchor === ya._id && (xa.mirror || xa.side !== 'top')) || + xa.mirror === 'all' || xa.mirror === 'allticks' || + (xa.mirrors && xa.mirrors[ya._id + 'bottom']), + bottompos = ya._length + gs.p + xlw / 2, + showtop = + (xa.anchor === ya._id && (xa.mirror || xa.side === 'top')) || + xa.mirror === 'all' || xa.mirror === 'allticks' || + (xa.mirrors && xa.mirrors[ya._id + 'top']), + toppos = -gs.p - xlw / 2, + + // shorten y axis lines so they don't overlap x axis lines + yp = gs.p, + // except where there's no x line + // TODO: this gets more complicated with multiple x and y axes + ypbottom = showbottom ? 0 : xlw, + yptop = showtop ? 0 : xlw, + ypathSuffix = ',' + (-yp - yptop) + + 'v' + (ya._length + 2 * yp + yptop + ypbottom), + showfreey = ya.anchor === 'free' && + freefinished.indexOf(ya._id) === -1, + freeposy = gs.w * (ya.position||0) + ((ylw / 2) % 1), + showleft = + (ya.anchor === xa._id && (ya.mirror || ya.side !== 'right')) || + ya.mirror === 'all' || ya.mirror === 'allticks' || + (ya.mirrors && ya.mirrors[xa._id + 'left']), + leftpos = -gs.p - ylw / 2, + showright = + (ya.anchor === xa._id && (ya.mirror || ya.side === 'right')) || + ya.mirror === 'all' || ya.mirror === 'allticks' || + (ya.mirrors && ya.mirrors[xa._id + 'right']), + rightpos = xa._length + gs.p + ylw / 2; + + // save axis line positions for ticks, draggers, etc to reference + // each subplot gets an entry: + // [left or bottom, right or top, free, main] + // main is the position at which to draw labels and draggers, if any + xa._linepositions[subplot] = [ + showbottom ? bottompos : undefined, + showtop ? toppos : undefined, + showfreex ? freeposx : undefined + ]; + if(xa.anchor === ya._id) { + xa._linepositions[subplot][3] = xa.side === 'top' ? + toppos : bottompos; + } + else if(showfreex) { + xa._linepositions[subplot][3] = freeposx; + } + + ya._linepositions[subplot] = [ + showleft ? leftpos : undefined, + showright ? rightpos : undefined, + showfreey ? freeposy : undefined + ]; + if(ya.anchor === xa._id) { + ya._linepositions[subplot][3] = ya.side === 'right' ? + rightpos : leftpos; + } + else if(showfreey) { + ya._linepositions[subplot][3] = freeposy; + } + + // translate all the extra stuff to have the + // same origin as the plot area or axes + var origin = 'translate(' + xa._offset + ',' + ya._offset + ')', + originx = origin, + originy = origin; + if(showfreex) { + originx = 'translate(' + xa._offset + ',' + gs.t + ')'; + toppos += ya._offset - gs.t; + bottompos += ya._offset - gs.t; + } + if(showfreey) { + originy = 'translate(' + gs.l + ',' + ya._offset + ')'; + leftpos += xa._offset - gs.l; + rightpos += xa._offset - gs.l; + } + + plotinfo.xlines + .attr('transform', originx) + .attr('d', ( + (showbottom ? (xpathPrefix + bottompos + xpathSuffix) : '') + + (showtop ? (xpathPrefix + toppos + xpathSuffix) : '') + + (showfreex ? (xpathPrefix + freeposx + xpathSuffix) : '')) || + // so it doesn't barf with no lines shown + 'M0,0') + .style('stroke-width', xlw + 'px') + .call(Color.stroke, xa.showline ? + xa.linecolor : 'rgba(0,0,0,0)'); + plotinfo.ylines + .attr('transform', originy) + .attr('d', ( + (showleft ? ('M' + leftpos + ypathSuffix) : '') + + (showright ? ('M' + rightpos + ypathSuffix) : '') + + (showfreey ? ('M' + freeposy + ypathSuffix) : '')) || + 'M0,0') + .attr('stroke-width', ylw + 'px') + .call(Color.stroke, ya.showline ? + ya.linecolor : 'rgba(0,0,0,0)'); + + plotinfo.xaxislayer.attr('transform', originx); + plotinfo.yaxislayer.attr('transform', originy); + plotinfo.gridlayer.attr('transform', origin); + plotinfo.zerolinelayer.attr('transform', origin); + plotinfo.draglayer.attr('transform', origin); + + // mark free axes as displayed, so we don't draw them again + if(showfreex) { freefinished.push(xa._id); } + if(showfreey) { freefinished.push(ya._id); } + }); + + Plotly.Axes.makeClipPaths(gd); + exports.drawMainTitle(gd); + ModeBar.manage(gd); + + return gd._promises.length && Promise.all(gd._promises); +}; + +exports.drawMainTitle = function(gd) { + var fullLayout = gd._fullLayout; + + Titles.draw(gd, 'gtitle', { + propContainer: fullLayout, + propName: 'title', + dfltName: 'Plot', + attributes: { + x: fullLayout.width / 2, + y: fullLayout._size.t / 2, + 'text-anchor': 'middle' + } + }); +}; + +// First, see if we need to do arraysToCalcdata +// call it regardless of what change we made, in case +// supplyDefaults brought in an array that was already +// in gd.data but not in gd._fullData previously +exports.doTraceStyle = function(gd) { + for(var i = 0; i < gd.calcdata.length; i++) { + var cdi = gd.calcdata[i], + _module = ((cdi[0] || {}).trace || {})._module || {}, + arraysToCalcdata = _module.arraysToCalcdata; + + if(arraysToCalcdata) arraysToCalcdata(cdi); + } + + Plots.style(gd); + Registry.getComponentMethod('legend', 'draw')(gd); + + return Plots.previousPromises(gd); +}; + +exports.doColorBars = function(gd) { + for(var i = 0; i < gd.calcdata.length; i++) { + var cdi0 = gd.calcdata[i][0]; + + if((cdi0.t || {}).cb) { + var trace = cdi0.trace, + cb = cdi0.t.cb; + + if(Registry.traceIs(trace, 'contour')) { + cb.line({ + width: trace.contours.showlines !== false ? + trace.line.width : 0, + dash: trace.line.dash, + color: trace.contours.coloring === 'line' ? + cb._opts.line.color : trace.line.color + }); + } + if(Registry.traceIs(trace, 'markerColorscale')) { + cb.options(trace.marker.colorbar)(); + } + else cb.options(trace.colorbar)(); + } + } + + return Plots.previousPromises(gd); +}; + +// force plot() to redo the layout and replot with the modified layout +exports.layoutReplot = function(gd) { + var layout = gd.layout; + gd.layout = undefined; + return Plotly.plot(gd, '', layout); +}; + +exports.doLegend = function(gd) { + Registry.getComponentMethod('legend', 'draw')(gd); + return Plots.previousPromises(gd); +}; + +exports.doTicksRelayout = function(gd) { + Plotly.Axes.doTicks(gd, 'redraw'); + exports.drawMainTitle(gd); + return Plots.previousPromises(gd); +}; + +exports.doModeBar = function(gd) { + var fullLayout = gd._fullLayout; + var subplotIds, i; + + ModeBar.manage(gd); + Plotly.Fx.supplyLayoutDefaults(gd.layout, gd._fullLayout, gd._fullData); + Plotly.Fx.init(gd); + + subplotIds = Plots.getSubplotIds(fullLayout, 'gl3d'); + for(i = 0; i < subplotIds.length; i++) { + var scene = fullLayout[subplotIds[i]]._scene; + scene.updateFx(fullLayout.dragmode, fullLayout.hovermode); + } + + subplotIds = Plots.getSubplotIds(fullLayout, 'gl2d'); + for(i = 0; i < subplotIds.length; i++) { + var scene2d = fullLayout._plots[subplotIds[i]]._scene2d; + scene2d.updateFx(fullLayout); + } + + subplotIds = Plots.getSubplotIds(fullLayout, 'geo'); + for(i = 0; i < subplotIds.length; i++) { + var geo = fullLayout[subplotIds[i]]._geo; + geo.updateFx(fullLayout.hovermode); + } + + return Plots.previousPromises(gd); +}; + +exports.setRangeSliderRange = function(gd, changes) { + var fullLayout = gd._fullLayout; + + var newMin = changes['xaxis.range'] ? changes['xaxis.range'][0] : changes['xaxis.range[0]'], + newMax = changes['xaxis.range'] ? changes['xaxis.range'][1] : changes['xaxis.range[1]']; + + var rangeSlider = fullLayout.xaxis && fullLayout.xaxis.rangeslider ? + fullLayout.xaxis.rangeslider : {}; + + if(rangeSlider.visible) { + if(newMin || newMax) { + fullLayout.xaxis.rangeslider.setRange(newMin, newMax); + } + else if(changes['xaxis.autorange']) { + fullLayout.xaxis.rangeslider.setRange(); + } + } +}; diff --git a/src/plots/plots.js b/src/plots/plots.js index b4948875f61..a38c9a13744 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -1348,27 +1348,18 @@ plots.computeFrame = function(gd, frameName) { * an array of data objects following the normal Plotly data definition format * @param {Object} layout * a layout object, following normal Plotly layout format - * @param {Number[]} traceIndices + * @param {Number[]} traces * indices of the corresponding traces specified in `data` * @param {Object} frameOpts * options for the frame (i.e. whether to redraw post-transition) * @param {Object} transitionOpts * options for the transition */ -plots.transition = function(gd, data, layout, traceIndices, frameOpts, transitionOpts) { +plots.transition = function(gd, data, layout, traces, frameOpts, transitionOpts) { var i, traceIdx; var dataLength = Array.isArray(data) ? data.length : 0; - - // Select which traces will be updated: - if(isNumeric(traceIndices)) traceIndices = [traceIndices]; - else if(!Array.isArray(traceIndices) || !traceIndices.length) { - traceIndices = gd.data.map(function(v, i) { return i; }); - } - - if(traceIndices.length > dataLength) { - traceIndices = traceIndices.slice(0, dataLength); - } + var traceIndices = traces.slice(0, dataLength); var transitionedTraces = []; diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index ac938f8358e..b5cde2401f7 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -6,6 +6,7 @@ var Scatter = require('@src/traces/scatter'); var Bar = require('@src/traces/bar'); var Legend = require('@src/components/legend'); var pkg = require('../../../package.json'); +var subroutines = require('@src/plot_api/subroutines'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); @@ -893,4 +894,80 @@ describe('Test plot api', function() { expect(gd.data[1].contours).toBeUndefined(); }); }); + + describe('Plotly.update should', function() { + var gd, calcdata; + + beforeAll(function() { + Object.keys(subroutines).forEach(function(k) { + spyOn(subroutines, k).and.callThrough(); + }); + }); + + beforeEach(function(done) { + gd = createGraphDiv(); + Plotly.plot(gd, [{ y: [2, 1, 2] }]).then(function() { + calcdata = gd.calcdata; + done(); + }); + }); + + afterEach(destroyGraphDiv); + + it('call doTraceStyle on trace style updates', function(done) { + expect(subroutines.doTraceStyle).not.toHaveBeenCalled(); + + Plotly.update(gd, { 'marker.color': 'blue' }).then(function() { + expect(subroutines.doTraceStyle).toHaveBeenCalledTimes(1); + expect(calcdata).toBe(gd.calcdata); + done(); + }); + }); + + it('clear calcdata on data updates', function(done) { + Plotly.update(gd, { x: [[3, 1, 3]] }).then(function() { + expect(calcdata).not.toBe(gd.calcdata); + done(); + }); + }); + + it('call doLegend on legend updates', function(done) { + expect(subroutines.doLegend).not.toHaveBeenCalled(); + + Plotly.update(gd, {}, { 'showlegend': true }).then(function() { + expect(subroutines.doLegend).toHaveBeenCalledTimes(1); + expect(calcdata).toBe(gd.calcdata); + done(); + }); + }); + + it('call layoutReplot when adding update menu', function(done) { + expect(subroutines.layoutReplot).not.toHaveBeenCalled(); + + var layoutUpdate = { + updatemenus: [{ + buttons: [{ + method: 'relayout', + args: ['title', 'Hello World'] + }] + }] + }; + + Plotly.update(gd, {}, layoutUpdate).then(function() { + expect(subroutines.doLegend).toHaveBeenCalledTimes(1); + expect(calcdata).toBe(gd.calcdata); + done(); + }); + }); + + it('call doModeBar when updating \'dragmode\'', function(done) { + expect(subroutines.doModeBar).not.toHaveBeenCalled(); + + Plotly.update(gd, {}, { 'dragmode': 'pan' }).then(function() { + expect(subroutines.doModeBar).toHaveBeenCalledTimes(1); + expect(calcdata).toBe(gd.calcdata); + done(); + }); + }); + }); }); diff --git a/test/jasmine/tests/transition_test.js b/test/jasmine/tests/transition_test.js index 76d261a480f..63deb99a688 100644 --- a/test/jasmine/tests/transition_test.js +++ b/test/jasmine/tests/transition_test.js @@ -1,6 +1,7 @@ var Plotly = require('@lib/index'); var Lib = require('@src/lib'); var Plots = Plotly.Plots; +var plotApiHelpers = require('@src/plot_api/helpers'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); @@ -29,8 +30,9 @@ function runTests(transitionDuration) { it('resolves only once the transition has completed', function(done) { var t1 = Date.now(); + var traces = plotApiHelpers.coerceTraceIndices(gd, null); - Plots.transition(gd, null, {'xaxis.range': [0.2, 0.3]}, null, {redraw: true}, {duration: transitionDuration, easing: 'cubic-in-out'}) + Plots.transition(gd, null, {'xaxis.range': [0.2, 0.3]}, traces, {redraw: true}, {duration: transitionDuration, easing: 'cubic-in-out'}) .then(delay(20)) .then(function() { expect(Date.now() - t1).toBeGreaterThan(transitionDuration); @@ -39,9 +41,11 @@ function runTests(transitionDuration) { it('emits plotly_transitioning on transition start', function(done) { var beginTransitionCnt = 0; + var traces = plotApiHelpers.coerceTraceIndices(gd, null); + gd.on('plotly_transitioning', function() { beginTransitionCnt++; }); - Plots.transition(gd, null, {'xaxis.range': [0.2, 0.3]}, null, {redraw: true}, {duration: transitionDuration, easing: 'cubic-in-out'}) + Plots.transition(gd, null, {'xaxis.range': [0.2, 0.3]}, traces, {redraw: true}, {duration: transitionDuration, easing: 'cubic-in-out'}) .then(delay(20)) .then(function() { expect(beginTransitionCnt).toBe(1); @@ -50,9 +54,11 @@ function runTests(transitionDuration) { it('emits plotly_transitioned on transition end', function(done) { var trEndCnt = 0; + var traces = plotApiHelpers.coerceTraceIndices(gd, null); + gd.on('plotly_transitioned', function() { trEndCnt++; }); - Plots.transition(gd, null, {'xaxis.range': [0.2, 0.3]}, null, {redraw: true}, {duration: transitionDuration, easing: 'cubic-in-out'}) + Plots.transition(gd, null, {'xaxis.range': [0.2, 0.3]}, traces, {redraw: true}, {duration: transitionDuration, easing: 'cubic-in-out'}) .then(delay(20)) .then(function() { expect(trEndCnt).toEqual(1); @@ -70,7 +76,8 @@ function runTests(transitionDuration) { gd.on('plotly_transitioned', function() { currentlyRunning--; endCnt++; }); function doTransition() { - return Plots.transition(gd, [{x: [1, 2]}], null, null, {redraw: true}, {duration: transitionDuration, easing: 'cubic-in-out'}); + var traces = plotApiHelpers.coerceTraceIndices(gd, null); + return Plots.transition(gd, [{x: [1, 2]}], null, traces, {redraw: true}, {duration: transitionDuration, easing: 'cubic-in-out'}); } function checkNoneRunning() {