From 17e7e793dd3d2f7cf852f997efd9a0267163cda9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 22 Aug 2016 13:56:05 -0400 Subject: [PATCH 01/13] plot api: move common routine to helpers.js --- src/plot_api/helpers.js | 401 +++++++++++++++++++++++++++++++++++++ src/plot_api/plot_api.js | 418 ++------------------------------------- 2 files changed, 420 insertions(+), 399 deletions(-) create mode 100644 src/plot_api/helpers.js diff --git a/src/plot_api/helpers.js b/src/plot_api/helpers.js new file mode 100644 index 00000000000..06e1b31594c --- /dev/null +++ b/src/plot_api/helpers.js @@ -0,0 +1,401 @@ +/** +* 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 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('+'); + } +}; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index d8c7b07a1cc..c035bfbf75d 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -10,7 +10,6 @@ 'use strict'; var d3 = require('d3'); -var m4FromQuat = require('gl-mat4/fromQuat'); var isNumeric = require('fast-isnumeric'); var Plotly = require('../plotly'); @@ -31,6 +30,8 @@ var ModeBar = require('../components/modebar'); var xmlnsNamespaces = require('../constants/xmlns_namespaces'); var svgTextUtils = require('../lib/svg_text_utils'); +var helpers = require('./helpers'); + /** * Main plot-creation function @@ -49,8 +50,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); @@ -91,7 +91,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); @@ -102,7 +102,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. @@ -329,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'); @@ -495,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)) { Lib.warn('This element is not a Plotly plot.', gd); @@ -843,7 +490,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 || {}); @@ -1250,7 +897,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, @@ -1277,7 +924,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, @@ -1313,7 +960,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, @@ -1330,7 +977,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) { @@ -1384,7 +1031,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, @@ -1450,7 +1097,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 = [], @@ -1540,8 +1187,8 @@ Plotly.moveTraces = function moveTraces(gd, currentIndices, newIndices) { // 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); + gd = helpers.getGraphDiv(gd); + helpers.clearPromiseQueue(gd); var i, fullLayout = gd._fullLayout, aobj = {}; @@ -1875,7 +1522,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); @@ -2044,33 +1691,6 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { 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('+'); - } } // relayout: change layout in an existing plot @@ -2085,8 +1705,8 @@ function swapXYData(trace) { // aobj - {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); @@ -2513,7 +2133,7 @@ Plotly.relayout = function relayout(gd, astr, val) { * 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 || []; From b2a072a64a23bf030e8c47d48c0a467b772a3b4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 22 Aug 2016 14:35:37 -0400 Subject: [PATCH 02/13] plot api: add proper jsDoc to restyle and relayout --- src/plot_api/plot_api.js | 82 +++++++++++++++++++++++----------------- 1 file changed, 48 insertions(+), 34 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index c035bfbf75d..a163db62265 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -1163,29 +1163,34 @@ 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 id or DOM element} gd + * the id or DOM element of the graph container div + * @param {string} astr + * attribute string (like `'marker.symbol'`) to update + * @param {any} val + * value to give this attribute + * @param {number or array} traces (optional) + * integer or array of integers for the traces to alter (all if omitted) + * + * Signature 2: + * @param {string id or DOM element} gd (as in signature 1) + * @param {object} aobj + * attribute object `{astr1: val1, astr2: val2 ...}` + * allows setting multiple attributes simultaneously + * @param {number or array} traces (optional, 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 = helpers.getGraphDiv(gd); helpers.clearPromiseQueue(gd); @@ -1693,17 +1698,26 @@ Plotly.restyle = function restyle(gd, astr, val, 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 id or dom element} gd + * the id or dom element of the graph container div + * @param {string} astr + * attribute string (like `'xaxis.range[0]'`) to update + * @param {any} val + * value to give this attribute + * + * Signature 2: + * @param {string id or DOM element} gd (as in signature 1) + * the id or DOM element of the graph container div + * @param {object} aobj + * attribute object `{astr1: val1, astr2: val2 ...}` + * allows setting multiple attributes simultaneously + */ Plotly.relayout = function relayout(gd, astr, val) { gd = helpers.getGraphDiv(gd); helpers.clearPromiseQueue(gd); From 3024b716db4789a29dd2bd98dc5d49d275a79089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 22 Aug 2016 16:40:21 -0400 Subject: [PATCH 03/13] plot api: move plot/update sub-routine to subroutines.js - this move will allow us to test (with jasmine spies) that the proper subroutines are called. --- src/plot_api/plot_api.js | 219 +---------------------- src/plot_api/subroutines.js | 339 ++++++++++++++++++++++++++++++++++++ 2 files changed, 343 insertions(+), 215 deletions(-) create mode 100644 src/plot_api/subroutines.js diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index a163db62265..797fae6e695 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -22,15 +22,13 @@ 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'); /** @@ -196,7 +194,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); } @@ -316,7 +314,7 @@ Plotly.plot = function(gd, data, layout, config) { marginPushers, marginPushersAgain, positionAndAutorange, - layoutStyles, + subroutines.layoutStyles, drawAxes, drawData, finalDraw @@ -2373,7 +2371,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); @@ -2543,212 +2541,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(); + } + } +}; From ce51b218716ab6fd6b697cacf5d0c64e23651870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 22 Aug 2016 16:41:45 -0400 Subject: [PATCH 04/13] plot api: rm unused 'dolayout' flag in restyle --- src/plot_api/plot_api.js | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 797fae6e695..091c342a345 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -1279,15 +1279,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) @@ -1299,7 +1290,6 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { var docalc = false, docalcAutorange = false, doplot = false, - dolayout = false, dostyle = false, docolorbars = false; // copies of the change (and previous values of anything affected) @@ -1627,12 +1617,6 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { // 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); - }]; } else if(docalc || doplot || docalcAutorange) { seq = [Plotly.plot]; From 497203d79ab43a9cbc4344a19e31697b59d0240f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 22 Aug 2016 16:42:51 -0400 Subject: [PATCH 05/13] plot api: refactor Plotly.restyle - split flag-finding logic with plot-routine sequence building --- src/plot_api/plot_api.js | 223 +++++++++++++++++++-------------------- 1 file changed, 110 insertions(+), 113 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 091c342a345..02238844a43 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -1193,8 +1193,7 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { gd = helpers.getGraphDiv(gd); helpers.clearPromiseQueue(gd); - var i, fullLayout = gd._fullLayout, - aobj = {}; + var aobj = {}; if(typeof astr === 'string') aobj[astr] = val; else if(Lib.isPlainObject(astr)) { @@ -1208,10 +1207,71 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { if(Object.keys(aobj).length) gd.changed = true; + 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; + + // fill up traces if(isNumeric(traces)) traces = [traces]; else if(!Array.isArray(traces) || !traces.length) { - traces = gd.data.map(function(v, i) { return i; }); - } + traces = data.map(function(_, i) { return i; }); + } + + // 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, @@ -1251,8 +1311,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; } @@ -1266,6 +1327,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 = [ @@ -1286,24 +1348,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, - 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 @@ -1312,9 +1366,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 @@ -1334,7 +1390,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)) { @@ -1347,10 +1403,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 @@ -1361,6 +1413,7 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { param, oldVal, newVal; + redoit[ai] = vi; if(ai.substr(0, 6) === 'LAYOUT') { @@ -1371,18 +1424,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; @@ -1540,7 +1593,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 @@ -1548,11 +1601,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) { @@ -1561,13 +1614,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); } } @@ -1576,12 +1629,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); @@ -1590,9 +1648,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; } } @@ -1601,83 +1660,21 @@ 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; + // combine a few flags together; + if(flags.docalc || (flags.docalcAutorange && flags.autorangeOn)) { + flags.clearCalc = true; } - - // now update the graphics - // a complete layout redraw takes care of plot and - var seq; - } - else if(docalc || doplot || docalcAutorange) { - seq = [Plotly.plot]; - } - 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); - }); - } + if(flags.docalc || flags.doplot || flags.docalcAutorange) { + flags.fullReplot = true; } - 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; - }); + return { + flags: flags, + undoit: undoit, + redoit: redoit, + traces: traces, + eventData: Lib.extendDeepNoArrays([], [redoit, traces]) + }; } /** From e1a989fe3b930290ce284ce574b1f4f56bff1ab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 22 Aug 2016 16:43:32 -0400 Subject: [PATCH 06/13] plot api: rm unused annotation ref -> xref/yref translate step --- src/plot_api/plot_api.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 02238844a43..cf9fd0f463f 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -1740,12 +1740,6 @@ Plotly.relayout = function relayout(gd, astr, val) { } 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]]; } } From 822cf8c83f600f5c28e336645042ee99b0a86c84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 22 Aug 2016 16:45:19 -0400 Subject: [PATCH 07/13] plot api: refactor Plotly.relayout - in a similar way to Plotly.restyle --- src/plot_api/plot_api.js | 231 ++++++++++++++++++--------------------- 1 file changed, 105 insertions(+), 126 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index cf9fd0f463f..f1781bd9b2a 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -1705,17 +1705,7 @@ Plotly.relayout = function relayout(gd, astr, val) { 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 { @@ -1725,30 +1715,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]]; - } + 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. @@ -1772,8 +1816,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), @@ -1826,23 +1868,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); } @@ -1881,8 +1924,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 @@ -1912,7 +1955,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 @@ -1940,7 +1983,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)); @@ -1949,38 +1992,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' || @@ -1988,126 +2031,62 @@ 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; - // replot with the modified layout - return Plotly.plot(gd, '', layout); - }); } - 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(dolayoutstyle) seq.push(layoutStyles); - if(doticks) { - seq.push(function() { - Plotly.Axes.doTicks(gd, 'redraw'); - drawMainTitle(gd); - return Plots.previousPromises(gd); - }); - } - // this is decoupled enough it doesn't need async regardless - if(domodebar) { - var subplotIds; - ModeBar.manage(gd); - Plotly.Fx.supplyLayoutDefaults(gd.layout, fullLayout, gd._fullData); - Plotly.Fx.init(gd); - 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); - } - - subplotIds = Plots.getSubplotIds(fullLayout, 'geo'); - for(i = 0; i < subplotIds.length; i++) { - var geo = fullLayout[subplotIds[i]]._geo; - geo.updateFx(fullLayout.hovermode); - } - } } - 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(); - } - } } - var plotDone = Lib.syncOrAsync(seq, gd); if(!plotDone || !plotDone.then) plotDone = Promise.resolve(gd); return plotDone.then(function() { - var changes = Lib.extendDeep({}, redoit); - setRange(changes); - gd.emit('plotly_relayout', changes); return gd; }); From 7752eb481d963d92c964057ac92c0da2f3a098b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 22 Aug 2016 16:46:35 -0400 Subject: [PATCH 08/13] add Plotly.update method: - reuse flag-finding methods called in restyle & relayout - combine flags into subroutine sequences --- src/core.js | 1 + src/plot_api/plot_api.js | 61 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/src/core.js b/src/core.js index 046e186e438..7ef90d3a4f2 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/plot_api.js b/src/plot_api/plot_api.js index f1781bd9b2a..788c40d10b1 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -2069,24 +2069,85 @@ function _relayout(gd, aobj) { }; } +/** + * update: update trace and layout attributes of an existing plot + * + * @param {string id or DOM element} 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 or array} traces (optional) + * integer or array of integers for the traces to alter (all if omitted) + * + */ +Plotly.update = function update(gd, traceUpdate, layoutUpdate, indices) { + gd = helpers.getGraphDiv(gd); + helpers.clearPromiseQueue(gd); + if(gd.framework && gd.framework.isPolar) { + return Promise.resolve(gd); } + if(!Lib.isPlainObject(traceUpdate)) traceUpdate = {}; + if(!Lib.isPlainObject(layoutUpdate)) layoutUpdate = {}; + if(Object.keys(traceUpdate).length) gd.changed = true; + if(Object.keys(layoutUpdate).length) gd.changed = true; + var restyleSpecs = _restyle(gd, traceUpdate, indices), + restyleFlags = restyleSpecs.flags; + var relayoutSpecs = _relayout(gd, layoutUpdate), + relayoutFlags = relayoutSpecs.flags; + // clear calcdata if required + if(restyleFlags.clearCalc || relayoutFlags.docalc) gd.calcdata = undefined; + // fill in redraw sequence + var seq = []; + 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); + 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); } + 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() { + subroutines.setRangeSliderRange(gd, relayoutSpecs.eventData); + gd.emit('plotly_update', { + data: restyleSpecs.eventData, + layout: relayoutSpecs.eventData + }); return gd; }); From 26c407597c22d4ba749b06d70a40e0ce3db3f6db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 22 Aug 2016 16:47:17 -0400 Subject: [PATCH 09/13] test: add test for Plotly.update - testing that the correct subroutines are called. --- test/jasmine/tests/plot_api_test.js | 77 +++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index 78fefeb75da..5cf25e78187 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'); @@ -879,4 +880,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(); + }); + }); + }); }); From 01967d8baa4b20da2032ace04b204a5121f5931a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 6 Sep 2016 17:10:09 -0400 Subject: [PATCH 10/13] merge followup: use helpers.getGraphDiv in animate/frame methods --- src/plot_api/plot_api.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index d325554fc2b..c6a070b8d02 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -2130,7 +2130,7 @@ Plotly.update = function update(gd, traceUpdate, layoutUpdate, indices) { * 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); @@ -2413,7 +2413,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); @@ -2500,7 +2500,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); From e8dbf55c6d450a83c91cd76473b46fa611980ab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 6 Sep 2016 17:10:44 -0400 Subject: [PATCH 11/13] improve jsDoc for restyle/relayout/update --- src/plot_api/plot_api.js | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index c6a070b8d02..1e2227ca71d 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -1115,21 +1115,23 @@ Plotly.moveTraces = function moveTraces(gd, currentIndices, newIndices) { * Can be called two ways. * * Signature 1: - * @param {string id or DOM element} gd + * @param {String | HTMLDivElement} gd * the id or DOM element of the graph container div - * @param {string} astr + * @param {String} astr * attribute string (like `'marker.symbol'`) to update - * @param {any} val + * @param {*} val * value to give this attribute - * @param {number or array} traces (optional) + * @param {Number[] | Number} [traces] * integer or array of integers for the traces to alter (all if omitted) * * Signature 2: - * @param {string id or DOM element} gd (as in signature 1) - * @param {object} aobj + * @param {String | HTMLDivElement} gd + * (as in signature 1) + * @param {Object} aobj * attribute object `{astr1: val1, astr2: val2 ...}` * allows setting multiple attributes simultaneously - * @param {number or array} traces (optional, as in signature 1) + * @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. @@ -1142,11 +1144,11 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { 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); @@ -1632,17 +1634,17 @@ function _restyle(gd, aobj, traces) { * Can be called two ways: * * Signature 1: - * @param {string id or dom element} gd + * @param {String | HTMLDivElement} gd * the id or dom element of the graph container div - * @param {string} astr + * @param {String} astr * attribute string (like `'xaxis.range[0]'`) to update - * @param {any} val + * @param {*} val * value to give this attribute * * Signature 2: - * @param {string id or DOM element} gd (as in signature 1) - * the id or DOM element of the graph container div - * @param {object} aobj + * @param {String | HTMLDivElement} gd + * (as in signature 1) + * @param {Object} aobj * attribute object `{astr1: val1, astr2: val2 ...}` * allows setting multiple attributes simultaneously */ @@ -2021,15 +2023,15 @@ function _relayout(gd, aobj) { /** * update: update trace and layout attributes of an existing plot * - * @param {string id or DOM element} gd + * @param {String | HTMLDivElement} gd * the id or DOM element of the graph container div - * @param {object} traceUpdate + * @param {Object} traceUpdate * attribute object `{astr1: val1, astr2: val2 ...}` * corresponding to updates in the plot's traces - * @param {object} layoutUpdate + * @param {Object} layoutUpdate * attribute object `{astr1: val1, astr2: val2 ...}` * corresponding to updates in the plot's layout - * @param {number or array} traces (optional) + * @param {Number[] | Number} [traces] * integer or array of integers for the traces to alter (all if omitted) * */ From 3db6d2145276e4bab80db29f2706a4f2904b6ea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 6 Sep 2016 17:13:02 -0400 Subject: [PATCH 12/13] dry: pull trace indices input -> list logic in helpers - use it in Plotly.restyle and Plotly.animate - call trace indices argument 'traces' consistently in restyle, update, and transition (to be consistent with the frame attribute 'traces'). --- src/plot_api/helpers.js | 13 +++++++++++++ src/plot_api/plot_api.js | 14 +++++--------- src/plots/plots.js | 15 +++------------ 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/plot_api/helpers.js b/src/plot_api/helpers.js index 06e1b31594c..35d9a0495f1 100644 --- a/src/plot_api/helpers.js +++ b/src/plot_api/helpers.js @@ -9,6 +9,7 @@ 'use strict'; +var isNumeric = require('fast-isnumeric'); var m4FromQuat = require('gl-mat4/fromQuat'); var Registry = require('../registry'); @@ -399,3 +400,15 @@ exports.swapXYData = function(trace) { 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 1e2227ca71d..2a7f445708b 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -1192,17 +1192,13 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { }); }; -function _restyle(gd, aobj, traces) { +function _restyle(gd, aobj, _traces) { var fullLayout = gd._fullLayout, fullData = gd._fullData, data = gd.data, i; - // fill up traces - if(isNumeric(traces)) traces = [traces]; - else if(!Array.isArray(traces) || !traces.length) { - traces = data.map(function(_, i) { return i; }); - } + var traces = helpers.coerceTraceIndices(gd, _traces); // initialize flags var flags = { @@ -2035,7 +2031,7 @@ function _relayout(gd, aobj) { * integer or array of integers for the traces to alter (all if omitted) * */ -Plotly.update = function update(gd, traceUpdate, layoutUpdate, indices) { +Plotly.update = function update(gd, traceUpdate, layoutUpdate, traces) { gd = helpers.getGraphDiv(gd); helpers.clearPromiseQueue(gd); @@ -2049,7 +2045,7 @@ Plotly.update = function update(gd, traceUpdate, layoutUpdate, indices) { if(Object.keys(traceUpdate).length) gd.changed = true; if(Object.keys(layoutUpdate).length) gd.changed = true; - var restyleSpecs = _restyle(gd, traceUpdate, indices), + var restyleSpecs = _restyle(gd, traceUpdate, traces), restyleFlags = restyleSpecs.flags; var relayoutSpecs = _relayout(gd, layoutUpdate), @@ -2286,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 ); 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 = []; From c15a617bf053f14d03855c35bbe2ae19becb51f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 6 Sep 2016 17:58:42 -0400 Subject: [PATCH 13/13] test: adapt transition test to more strict 'traces' API --- test/jasmine/tests/transition_test.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) 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() {