From cc45189d12063895b91101667586d8dc7317a76d Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 6 Jan 2017 13:05:29 -0500 Subject: [PATCH 01/18] fix basic annotation reference change bug --- src/components/annotations/draw.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/annotations/draw.js b/src/components/annotations/draw.js index f6cb37b8515..dd174efa02f 100644 --- a/src/components/annotations/draw.js +++ b/src/components/annotations/draw.js @@ -157,7 +157,7 @@ function drawOne(gd, index, opt, value) { if(optionsIn.visible === false) return; var gs = fullLayout._size; - var oldRef = {xref: optionsIn.xref, yref: optionsIn.yref}; + var oldRef = {xref: oldPrivate.xref, yref: oldPrivate.yref}; var axLetters = ['x', 'y']; for(i = 0; i < 2; i++) { From 13a87cea3037c168075f7934c74b20e48b19c3b9 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sat, 18 Feb 2017 01:24:31 -0500 Subject: [PATCH 02/18] support RegExp in Lib.pushUnique --- src/lib/index.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/lib/index.js b/src/lib/index.js index 9544f4b3794..6291543317c 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -17,6 +17,7 @@ lib.nestedProperty = require('./nested_property'); lib.isPlainObject = require('./is_plain_object'); lib.isArray = require('./is_array'); lib.mod = require('./mod'); +lib.toLogRange = require('./to_log_range'); var coerceModule = require('./coerce'); lib.valObjects = coerceModule.valObjects; @@ -349,7 +350,17 @@ lib.noneOrAll = function(containerIn, containerOut, attrList) { * */ lib.pushUnique = function(array, item) { - if(item && array.indexOf(item) === -1) array.push(item); + if(item instanceof RegExp) { + var itemStr = item.toString(), + i; + for(i = 0; i < array.length; i++) { + if(array[i] instanceof RegExp && array[i].toString() === itemStr) { + return array; + } + } + array.push(item); + } + else if(item && array.indexOf(item) === -1) array.push(item); return array; }; From 61f250c66bd2d814187b7aaf16fe965a4abe66a5 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 9 Jan 2017 22:29:55 -0500 Subject: [PATCH 03/18] editContainerArray: consistent handling of container array edits supported in all relayout edits, not yet in restyle edits start on editComponentArray more baby steps more partial... finish up refactor - but still need to test edge cases of linear -> log delete shapes from all layers in case index changed support editContainerArray for all layout container arrays --- src/components/annotations/convert_coords.js | 59 ++++ src/components/annotations/draw.js | 201 +------------- src/components/annotations/index.js | 4 +- src/components/images/convert_coords.js | 79 ++++++ src/components/images/index.js | 4 +- src/components/shapes/draw.js | 186 +------------ src/components/updatemenus/attributes.js | 1 + src/lib/nested_property.js | 73 +++-- src/lib/to_log_range.js | 26 ++ src/plot_api/helpers.js | 18 +- src/plot_api/manage_arrays.js | 243 +++++++++++++++++ src/plot_api/plot_api.js | 244 ++++++++++------- src/plot_api/plot_schema.js | 3 +- src/plots/mapbox/layout_attributes.js | 1 + src/registry.js | 29 +- test/image/mocks/layout_image.json | 1 + test/jasmine/tests/annotations_test.js | 266 ++++++++++++++++++- test/jasmine/tests/layout_images_test.js | 127 ++++++++- test/jasmine/tests/lib_test.js | 40 ++- test/jasmine/tests/mapbox_test.js | 44 +-- test/jasmine/tests/shapes_test.js | 55 +++- 21 files changed, 1164 insertions(+), 540 deletions(-) create mode 100644 src/components/annotations/convert_coords.js create mode 100644 src/components/images/convert_coords.js create mode 100644 src/lib/to_log_range.js create mode 100644 src/plot_api/manage_arrays.js diff --git a/src/components/annotations/convert_coords.js b/src/components/annotations/convert_coords.js new file mode 100644 index 00000000000..ba3441f0b2f --- /dev/null +++ b/src/components/annotations/convert_coords.js @@ -0,0 +1,59 @@ +/** +* Copyright 2012-2017, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var isNumeric = require('fast-isnumeric'); +var toLogRange = require('../../lib/to_log_range'); + +/* + * convertCoords: when converting an axis between log and linear + * you need to alter any annotations on that axis to keep them + * pointing at the same data point. + * In v2.0 this will become obsolete + * + * gd: the plot div + * ax: the axis being changed + * newType: the type it's getting + * doExtra: function(attr, val) from inside relayout that sets the attribute. + * Use this to make the changes as it's aware if any other changes in the + * same relayout call should override this conversion. + */ +module.exports = function convertCoords(gd, ax, newType, doExtra) { + var toLog = (newType === 'log') && (ax.type === 'linear'), + fromLog = (newType === 'linear') && (ax.type === 'log'); + + if(!(toLog || fromLog)) return; + + var annotations = gd._fullLayout.annotations, + axLetter = ax._id.charAt(0), + ann, + attrPrefix; + + function convert(attr) { + var currentVal = ann[attr], + newVal = null; + + if(toLog) newVal = toLogRange(currentVal, ax.range); + else newVal = Math.pow(10, currentVal); + + // if conversion failed, delete the value so it gets a default value + if(!isNumeric(newVal)) newVal = null; + + doExtra(attrPrefix + attr, newVal); + } + + for(var i = 0; i < annotations.length; i++) { + ann = annotations[i]; + attrPrefix = 'annotations[' + i + '].'; + + if(ann[axLetter + 'ref'] === ax._id) convert(axLetter); + if(ann['a' + axLetter + 'ref'] === ax._id) convert('a' + axLetter); + } +}; diff --git a/src/components/annotations/draw.js b/src/components/annotations/draw.js index dd174efa02f..97d95aa3f49 100644 --- a/src/components/annotations/draw.js +++ b/src/components/annotations/draw.js @@ -10,7 +10,6 @@ 'use strict'; var d3 = require('d3'); -var isNumeric = require('fast-isnumeric'); var Plotly = require('../../plotly'); var Plots = require('../../plots/plots'); @@ -22,8 +21,6 @@ var svgTextUtils = require('../../lib/svg_text_utils'); var setCursor = require('../../lib/setcursor'); var dragElement = require('../dragelement'); -var handleAnnotationDefaults = require('./annotation_defaults'); -var supplyLayoutDefaults = require('./defaults'); var drawArrowHead = require('./draw_arrow_head'); @@ -41,6 +38,9 @@ module.exports = { drawOne: drawOne }; +/* + * draw: draw all annotations without any new modifications + */ function draw(gd) { var fullLayout = gd._fullLayout; @@ -55,196 +55,26 @@ function draw(gd) { return Plots.previousPromises(gd); } -function drawOne(gd, index, opt, value) { +/* + * drawOne: draw a single annotation, potentially with modifications + * + * index (int): the annotation to draw + */ +function drawOne(gd, index) { var layout = gd.layout, fullLayout = gd._fullLayout, - i; - - if(!isNumeric(index) || index === -1) { - - // no index provided - we're operating on ALL annotations - if(!index && Array.isArray(value)) { - // a whole annotation array is passed in - // (as in, redo of delete all) - layout.annotations = value; - supplyLayoutDefaults(layout, fullLayout); - draw(gd); - return; - } - else if(value === 'remove') { - // delete all - delete layout.annotations; - fullLayout.annotations = []; - draw(gd); - return; - } - else if(opt && value !== 'add') { - // make the same change to all annotations - for(i = 0; i < fullLayout.annotations.length; i++) { - drawOne(gd, i, opt, value); - } - return; - } - else { - // add a new empty annotation - index = fullLayout.annotations.length; - fullLayout.annotations.push({}); - } - } - - if(!opt && value) { - if(value === 'remove') { - fullLayout._infolayer.selectAll('.annotation[data-index="' + index + '"]') - .remove(); - fullLayout.annotations.splice(index, 1); - layout.annotations.splice(index, 1); - for(i = index; i < fullLayout.annotations.length; i++) { - fullLayout._infolayer - .selectAll('.annotation[data-index="' + (i + 1) + '"]') - .attr('data-index', String(i)); - - // redraw all annotations past the removed one, - // so they bind to the right events - drawOne(gd, i); - } - return; - } - else if(value === 'add' || Lib.isPlainObject(value)) { - fullLayout.annotations.splice(index, 0, {}); - - var rule = Lib.isPlainObject(value) ? - Lib.extendFlat({}, value) : - {text: 'New text'}; - - if(layout.annotations) { - layout.annotations.splice(index, 0, rule); - } else { - layout.annotations = [rule]; - } - - for(i = fullLayout.annotations.length - 1; i > index; i--) { - fullLayout._infolayer - .selectAll('.annotation[data-index="' + (i - 1) + '"]') - .attr('data-index', String(i)); - drawOne(gd, i); - } - } - } + gs = gd._fullLayout._size; // remove the existing annotation if there is one fullLayout._infolayer.selectAll('.annotation[data-index="' + index + '"]').remove(); // remember a few things about what was already there, var optionsIn = layout.annotations[index], - oldPrivate = fullLayout.annotations[index]; - - // not sure how we're getting here... but C12 is seeing a bug - // where we fail here when they add/remove annotations - if(!optionsIn) return; + options = fullLayout.annotations[index]; - // alter the input annotation as requested - var optionsEdit = {}; - if(typeof opt === 'string' && opt) optionsEdit[opt] = value; - else if(Lib.isPlainObject(opt)) optionsEdit = opt; - - var optionKeys = Object.keys(optionsEdit); - for(i = 0; i < optionKeys.length; i++) { - var k = optionKeys[i]; - Lib.nestedProperty(optionsIn, k).set(optionsEdit[k]); - } - - // return early in visible: false updates - if(optionsIn.visible === false) return; - - var gs = fullLayout._size; - var oldRef = {xref: oldPrivate.xref, yref: oldPrivate.yref}; - - var axLetters = ['x', 'y']; - for(i = 0; i < 2; i++) { - var axLetter = axLetters[i]; - // if we don't have an explicit position already, - // don't set one just because we're changing references - // or axis type. - // the defaults will be consistent most of the time anyway, - // except in log/linear changes - if(optionsEdit[axLetter] !== undefined || - optionsIn[axLetter] === undefined) { - continue; - } - - var axOld = Axes.getFromId(gd, Axes.coerceRef(oldRef, {}, gd, axLetter, '', 'paper')), - axNew = Axes.getFromId(gd, Axes.coerceRef(optionsIn, {}, gd, axLetter, '', 'paper')), - position = optionsIn[axLetter], - axTypeOld = oldPrivate['_' + axLetter + 'type']; - - if(optionsEdit[axLetter + 'ref'] !== undefined) { - - // TODO: include ax / ay / axref / ayref here if not 'pixel' - // or even better, move all of this machinery out of here and into - // streambed as extra attributes to a regular relayout call - // we should do this after v2.0 when it can work equivalently for - // annotations, shapes, and images. - - var autoAnchor = optionsIn[axLetter + 'anchor'] === 'auto', - plotSize = (axLetter === 'x' ? gs.w : gs.h), - halfSizeFrac = (oldPrivate['_' + axLetter + 'size'] || 0) / - (2 * plotSize); - if(axOld && axNew) { // data -> different data - // go to the same fraction of the axis length - // whether or not these axes share a domain - - position = axNew.fraction2r(axOld.r2fraction(position)); - } - else if(axOld) { // data -> paper - // first convert to fraction of the axis - position = axOld.r2fraction(position); - - // next scale the axis to the whole plot - position = axOld.domain[0] + - position * (axOld.domain[1] - axOld.domain[0]); - - // finally see if we need to adjust auto alignment - // because auto always means middle / center alignment for data, - // but it changes for page alignment based on the closest side - if(autoAnchor) { - var posPlus = position + halfSizeFrac, - posMinus = position - halfSizeFrac; - if(position + posMinus < 2 / 3) position = posMinus; - else if(position + posPlus > 4 / 3) position = posPlus; - } - } - else if(axNew) { // paper -> data - // first see if we need to adjust auto alignment - if(autoAnchor) { - if(position < 1 / 3) position += halfSizeFrac; - else if(position > 2 / 3) position -= halfSizeFrac; - } - - // next convert to fraction of the axis - position = (position - axNew.domain[0]) / - (axNew.domain[1] - axNew.domain[0]); - - // finally convert to data coordinates - position = axNew.fraction2r(position); - } - } - - if(axNew && axNew === axOld && axTypeOld) { - if(axTypeOld === 'log' && axNew.type !== 'log') { - position = Math.pow(10, position); - } - else if(axTypeOld !== 'log' && axNew.type === 'log') { - position = (position > 0) ? - Math.log(position) / Math.LN10 : undefined; - } - } - - optionsIn[axLetter] = position; - } - - var options = {}; - handleAnnotationDefaults(optionsIn, options, fullLayout); - fullLayout.annotations[index] = options; + // this annotation is gone - quit now after deleting it + // TODO: use d3 idioms instead of deleting and redrawing every time + if(!optionsIn || options.visible === false) return; var xa = Axes.getFromId(gd, options.xref), ya = Axes.getFromId(gd, options.yref), @@ -457,9 +287,6 @@ function drawOne(gd, index, opt, value) { options['_' + axLetter + 'padplus'] = (annSize / 2) + textPadShift; options['_' + axLetter + 'padminus'] = (annSize / 2) - textPadShift; - - // save the current axis type for later log/linear changes - options['_' + axLetter + 'type'] = ax && ax.type; }); if(annotationIsOffscreen) { diff --git a/src/components/annotations/index.js b/src/components/annotations/index.js index bb32b6b69df..aea3d914aa6 100644 --- a/src/components/annotations/index.js +++ b/src/components/annotations/index.js @@ -24,5 +24,7 @@ module.exports = { drawOne: drawModule.drawOne, hasClickToShow: clickModule.hasClickToShow, - onClick: clickModule.onClick + onClick: clickModule.onClick, + + convertCoords: require('./convert_coords') }; diff --git a/src/components/images/convert_coords.js b/src/components/images/convert_coords.js new file mode 100644 index 00000000000..a2dd2a8608e --- /dev/null +++ b/src/components/images/convert_coords.js @@ -0,0 +1,79 @@ +/** +* Copyright 2012-2017, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var isNumeric = require('fast-isnumeric'); +var toLogRange = require('../../lib/to_log_range'); + +/* + * convertCoords: when converting an axis between log and linear + * you need to alter any images on that axis to keep them + * pointing at the same data point. + * In v2.0 this will become obsolete (or perhaps size will still need conversion?) + * we convert size by declaring that the maximum extent *in data units* should be + * the same, assuming the image is anchored by its center (could remove that restriction + * if we think it's important) even though the actual left and right values will not be + * quite the same since the scale becomes nonlinear (and central anchor means the pixel + * center of the image, not the data units center) + * + * gd: the plot div + * ax: the axis being changed + * newType: the type it's getting + * doExtra: function(attr, val) from inside relayout that sets the attribute. + * Use this to make the changes as it's aware if any other changes in the + * same relayout call should override this conversion. + */ +module.exports = function convertCoords(gd, ax, newType, doExtra) { + var toLog = (newType === 'log') && (ax.type === 'linear'), + fromLog = (newType === 'linear') && (ax.type === 'log'); + + if(!(toLog || fromLog)) return; + + var images = gd._fullLayout.images, + axLetter = ax._id.charAt(0), + image, + attrPrefix; + + for(var i = 0; i < images.length; i++) { + image = images[i]; + attrPrefix = 'images[' + i + '].'; + + if(image[axLetter + 'ref'] === ax._id) { + var currentPos = image[axLetter], + currentSize = image['size' + axLetter], + newPos = null, + newSize = null; + + if(toLog) { + newPos = toLogRange(currentPos, ax.range); + + // this is the inverse of the conversion we do in fromLog below + // so that the conversion is reversible (notice the fromLog conversion + // is like sinh, and this one looks like arcsinh) + var dx = currentSize / Math.pow(10, newPos) / 2; + newSize = 2 * Math.log(dx + Math.sqrt(1 + dx * dx)) / Math.LN10; + } + else { + newPos = Math.pow(10, currentPos); + newSize = newPos * (Math.pow(10, currentSize / 2) - Math.pow(10, -currentSize / 2)); + } + + // if conversion failed, delete the value so it can get a default later on + if(!isNumeric(newPos)) { + newPos = null; + newSize = null; + } + else if(!isNumeric(newSize)) newSize = null; + + doExtra(attrPrefix + axLetter, newPos); + doExtra(attrPrefix + 'size' + axLetter, newSize); + } + } +}; diff --git a/src/components/images/index.js b/src/components/images/index.js index d7ce308ae28..3a4269ef8df 100644 --- a/src/components/images/index.js +++ b/src/components/images/index.js @@ -15,5 +15,7 @@ module.exports = { layoutAttributes: require('./attributes'), supplyLayoutDefaults: require('./defaults'), - draw: require('./draw') + draw: require('./draw'), + + convertCoords: require('./convert_coords') }; diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index 314cd339f76..90eee257e5d 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -9,8 +9,6 @@ 'use strict'; -var isNumeric = require('fast-isnumeric'); - var Plotly = require('../../plotly'); var Lib = require('../../lib'); var Axes = require('../../plots/cartesian/axes'); @@ -22,8 +20,6 @@ var setCursor = require('../../lib/setcursor'); var constants = require('./constants'); var helpers = require('./helpers'); -var handleShapeDefaults = require('./shape_defaults'); -var supplyLayoutDefaults = require('./defaults'); // Shapes are stored in gd.layout.shapes, an array of objects @@ -58,183 +54,21 @@ function draw(gd) { // return Plots.previousPromises(gd); } -function drawOne(gd, index, opt, value) { - if(!isNumeric(index) || index === -1) { - - // no index provided - we're operating on ALL shapes - if(!index && Array.isArray(value)) { - replaceAllShapes(gd, value); - return; - } - else if(value === 'remove') { - deleteAllShapes(gd); - return; - } - else if(opt && value !== 'add') { - updateAllShapes(gd, opt, value); - return; - } - else { - // add a new empty annotation - index = gd._fullLayout.shapes.length; - gd._fullLayout.shapes.push({}); - } - } - - if(!opt && value) { - if(value === 'remove') { - deleteShape(gd, index); - return; - } - else if(value === 'add' || Lib.isPlainObject(value)) { - insertShape(gd, index, value); - } - } - - updateShape(gd, index, opt, value); -} - -function replaceAllShapes(gd, newShapes) { - gd.layout.shapes = newShapes; - supplyLayoutDefaults(gd.layout, gd._fullLayout); - draw(gd); -} - -function deleteAllShapes(gd) { - delete gd.layout.shapes; - gd._fullLayout.shapes = []; - draw(gd); -} - -function updateAllShapes(gd, opt, value) { - for(var i = 0; i < gd._fullLayout.shapes.length; i++) { - drawOne(gd, i, opt, value); - } -} - -function deleteShape(gd, index) { - getShapeLayer(gd, index) - .selectAll('[data-index="' + index + '"]') - .remove(); - - gd._fullLayout.shapes.splice(index, 1); - - gd.layout.shapes.splice(index, 1); - - for(var i = index; i < gd._fullLayout.shapes.length; i++) { - // redraw all shapes past the removed one, - // so they bind to the right events - getShapeLayer(gd, i) - .selectAll('[data-index="' + (i + 1) + '"]') - .attr('data-index', i); - drawOne(gd, i); - } -} - -function insertShape(gd, index, newShape) { - gd._fullLayout.shapes.splice(index, 0, {}); - - var rule = Lib.isPlainObject(newShape) ? - Lib.extendFlat({}, newShape) : - {text: 'New text'}; - - if(gd.layout.shapes) { - gd.layout.shapes.splice(index, 0, rule); - } else { - gd.layout.shapes = [rule]; - } - - // there is no need to call shapes.draw(gd, index), - // because updateShape() is called from within shapes.draw() - - for(var i = gd._fullLayout.shapes.length - 1; i > index; i--) { - getShapeLayer(gd, i) - .selectAll('[data-index="' + (i - 1) + '"]') - .attr('data-index', i); - drawOne(gd, i); - } -} - -function updateShape(gd, index, opt, value) { +function drawOne(gd, index) { var i, n; - // remove the existing shape if there is one - getShapeLayer(gd, index) - .selectAll('[data-index="' + index + '"]') + // remove the existing shape if there is one. + // because indices can change, we need to look in all shape layers + gd._fullLayout._paper + .selectAll('.shapelayer [data-index="' + index + '"]') .remove(); - // remember a few things about what was already there, - var optionsIn = gd.layout.shapes[index]; - - // (from annos...) not sure how we're getting here... but C12 is seeing a bug - // where we fail here when they add/remove annotations - // TODO: clean this up and remove it. - if(!optionsIn) return; - - // alter the input shape as requested - var optionsEdit = {}; - if(typeof opt === 'string' && opt) optionsEdit[opt] = value; - else if(Lib.isPlainObject(opt)) optionsEdit = opt; - - var optionKeys = Object.keys(optionsEdit); - for(i = 0; i < optionKeys.length; i++) { - var k = optionKeys[i]; - Lib.nestedProperty(optionsIn, k).set(optionsEdit[k]); - } - - // return early in visible: false updates - if(optionsIn.visible === false) return; - - var oldRef = {xref: optionsIn.xref, yref: optionsIn.yref}, - posAttrs = ['x0', 'x1', 'y0', 'y1']; - - for(i = 0; i < 4; i++) { - var posAttr = posAttrs[i]; - // if we don't have an explicit position already, - // don't set one just because we're changing references - // or axis type. - // the defaults will be consistent most of the time anyway, - // except in log/linear changes - if(optionsEdit[posAttr] !== undefined || - optionsIn[posAttr] === undefined) { - continue; - } - - var axLetter = posAttr.charAt(0), - axOld = Axes.getFromId(gd, - Axes.coerceRef(oldRef, {}, gd, axLetter, '', 'paper')), - axNew = Axes.getFromId(gd, - Axes.coerceRef(optionsIn, {}, gd, axLetter, '', 'paper')), - position = optionsIn[posAttr], - rangePosition; - - if(optionsEdit[axLetter + 'ref'] !== undefined) { - // first convert to fraction of the axis - if(axOld) { - rangePosition = helpers.shapePositionToRange(axOld)(position); - position = axOld.r2fraction(rangePosition); - } else { - position = (position - axNew.domain[0]) / - (axNew.domain[1] - axNew.domain[0]); - } - - if(axNew) { - // then convert to new data coordinates at the same fraction - rangePosition = axNew.fraction2r(position); - position = helpers.rangeToShapePosition(axNew)(rangePosition); - } else { - // or scale to the whole plot - position = axOld.domain[0] + - position * (axOld.domain[1] - axOld.domain[0]); - } - } - - optionsIn[posAttr] = position; - } + var optionsIn = gd.layout.shapes[index], + options = gd._fullLayout.shapes[index]; - var options = {}; - handleShapeDefaults(optionsIn, options, gd._fullLayout); - gd._fullLayout.shapes[index] = options; + // this shape is gone - quit now after deleting it + // TODO: use d3 idioms instead of deleting and redrawing every time + if(!optionsIn || options.visible === false) return; var clipAxes; if(options.layer !== 'below') { diff --git a/src/components/updatemenus/attributes.js b/src/components/updatemenus/attributes.js index f04465f9ec3..9dd9f9b155d 100644 --- a/src/components/updatemenus/attributes.js +++ b/src/components/updatemenus/attributes.js @@ -49,6 +49,7 @@ var buttonsAttrs = { module.exports = { _isLinkedToArray: 'updatemenu', + _arrayAttrRegexps: [/^updatemenus\[(0|[1-9][0-9]+)\]\.buttons/], visible: { valType: 'boolean', diff --git a/src/lib/nested_property.js b/src/lib/nested_property.js index a00cd17137a..1feb4ba5599 100644 --- a/src/lib/nested_property.js +++ b/src/lib/nested_property.js @@ -11,6 +11,8 @@ var isNumeric = require('fast-isnumeric'); var isArray = require('./is_array'); +var isPlainObject = require('./is_plain_object'); +var containerArrayMatch = require('../plot_api/manage_arrays').containerArrayMatch; /** * convert a string s (such as 'xaxis.range[0]') @@ -66,7 +68,7 @@ module.exports = function nestedProperty(container, propStr) { } return { - set: npSet(container, propParts), + set: npSet(container, propParts, propStr), get: npGet(container, propParts), astr: propStr, parts: propParts, @@ -118,19 +120,38 @@ function npGet(cont, parts) { * The ONLY case we are looking for is where the entire array is selected, parts[end] === 'x' * AND the replacement value is an array. */ -function isDataArray(val, key) { +// function isNotAContainer(key) { +// var containers = ['annotations', 'shapes', 'range', 'domain', 'buttons']; - var containers = ['annotations', 'shapes', 'range', 'domain', 'buttons'], - isNotAContainer = containers.indexOf(key) === -1; +// return containers.indexOf(key) === -1; +// } - return isArray(val) && isNotAContainer; +/* + * Can this value be deleted? We can delete any empty object (null, undefined, [], {}) + * EXCEPT empty data arrays. If it's not a data array, it's a container array, + * ie containing objects like annotations, buttons, etc + */ +var DOMAIN_RANGE = /(^|.)(domain|range)$/; +function isDeletable(val, propStr) { + if(!emptyObj(val)) return false; + if(!isArray(val)) return true; + + // domain and range are special - they show up in lots of places so hard code here. + if(propStr.match(DOMAIN_RANGE)) return true; + + var match = containerArrayMatch(propStr); + // if propStr matches the container array itself, index is an empty string + // otherwise we've matched something inside the container array, which may + // still be a data array. + return match && (match.index === ''); } -function npSet(cont, parts) { +function npSet(cont, parts, propStr) { return function(val) { var curCont = cont, - containerLevels = [cont], - toDelete = emptyObj(val) && !isDataArray(val, parts[parts.length - 1]), + propPart = '', + containerLevels = [[cont, propPart]], + toDelete = isDeletable(val, propStr), curPart, i; @@ -143,7 +164,7 @@ function npSet(cont, parts) { // handle special -1 array index if(curPart === -1) { - toDelete = !setArrayAll(curCont, parts.slice(i + 1), val); + toDelete = !setArrayAll(curCont, parts.slice(i + 1), val, propStr); if(toDelete) break; else return; } @@ -158,7 +179,9 @@ function npSet(cont, parts) { throw 'container is not an object'; } - containerLevels.push(curCont); + propPart = joinPropStr(propPart, curPart); + + containerLevels.push([curCont, propPart]); } if(toDelete) { @@ -169,25 +192,32 @@ function npSet(cont, parts) { }; } +function joinPropStr(propStr, newPart) { + if(!propStr) return newPart; + return propStr + isNumeric(newPart) ? ('[' + newPart + ']') : ('.' + newPart); +} + // handle special -1 array index -function setArrayAll(containerArray, innerParts, val) { +function setArrayAll(containerArray, innerParts, val, propStr) { var arrayVal = isArray(val), allSet = true, thisVal = val, - deleteThis = arrayVal ? false : emptyObj(val), + thisPropStr = propStr.replace('-1', 0), + deleteThis = arrayVal ? false : isDeletable(val, thisPropStr), firstPart = innerParts[0], i; for(i = 0; i < containerArray.length; i++) { + thisPropStr = propStr.replace('-1', i); if(arrayVal) { thisVal = val[i % val.length]; - deleteThis = emptyObj(thisVal); + deleteThis = isDeletable(thisVal, thisPropStr); } if(deleteThis) allSet = false; if(!checkNewContainer(containerArray, i, firstPart, deleteThis)) { continue; } - npSet(containerArray[i], innerParts)(thisVal); + npSet(containerArray[i], innerParts, propStr.replace('-1', i))(thisVal); } return allSet; } @@ -211,14 +241,21 @@ function pruneContainers(containerLevels) { var i, j, curCont, + propPart, keys, remainingKeys; for(i = containerLevels.length - 1; i >= 0; i--) { - curCont = containerLevels[i]; + curCont = containerLevels[i][0]; + propPart = containerLevels[i][1]; + remainingKeys = false; if(isArray(curCont)) { for(j = curCont.length - 1; j >= 0; j--) { - if(emptyObj(curCont[j])) { + // If there's a plain object in an array, it's a container array + // so we don't delete empty containers because they still have meaning. + // `editContainerArray` handles the API for adding/removing objects + // in this case. + if(emptyObj(curCont[j]) && !isPlainObject(curCont[j])) { if(remainingKeys) curCont[j] = undefined; else curCont.pop(); } @@ -229,7 +266,9 @@ function pruneContainers(containerLevels) { keys = Object.keys(curCont); remainingKeys = false; for(j = keys.length - 1; j >= 0; j--) { - if(emptyObj(curCont[keys[j]]) && !isDataArray(curCont[keys[j]], keys[j])) delete curCont[keys[j]]; + if(isDeletable(curCont[keys[j]], joinPropStr(propPart, keys[j]))) { + delete curCont[keys[j]]; + } else remainingKeys = true; } } diff --git a/src/lib/to_log_range.js b/src/lib/to_log_range.js new file mode 100644 index 00000000000..624eac2d896 --- /dev/null +++ b/src/lib/to_log_range.js @@ -0,0 +1,26 @@ +/** +* Copyright 2012-2017, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var isNumeric = require('fast-isnumeric'); + +/** + * convert a linear value into a logged value, folding negative numbers into + * the given range + */ +module.exports = function toLogRange(val, range) { + if(val > 0) return Math.log(val) / Math.LN10; + + // move a negative value reference to a log axis - just put the + // result at the lowest range value on the plot (or if the range also went negative, + // one millionth of the top of the range) + var newVal = Math.log(Math.min(range[0], range[1])) / Math.LN10; + if(!isNumeric(newVal)) newVal = Math.log(Math.max(range[0], range[1])) / Math.LN10 - 6; + return newVal; +}; diff --git a/src/plot_api/helpers.js b/src/plot_api/helpers.js index 62a4b7e38d5..64a265e354c 100644 --- a/src/plot_api/helpers.js +++ b/src/plot_api/helpers.js @@ -439,7 +439,7 @@ exports.coerceTraceIndices = function(gd, traceIndices) { /** * Manages logic around array container item creation / deletion / update - * that nested property along can't handle. + * that nested property alone can't handle. * * @param {Object} np * nested property of update attribute string about trace or layout object @@ -484,3 +484,19 @@ exports.manageArrayContainers = function(np, newVal, undoit) { np.set(newVal); } }; + +var ATTR_TAIL_RE = /(\.[^\[\]\.]+|\[[^\[\]\.]+\])$/; + +function getParent(attr) { + var tail = attr.search(ATTR_TAIL_RE); + if(tail > 0) return attr.substr(0, tail); +} + +exports.hasParent = function(aobj, attr) { + var attrParent = getParent(attr); + while(attrParent) { + if(attrParent in aobj) return true; + attrParent = getParent(attrParent); + } + return false; +}; diff --git a/src/plot_api/manage_arrays.js b/src/plot_api/manage_arrays.js new file mode 100644 index 00000000000..eff0d6284ab --- /dev/null +++ b/src/plot_api/manage_arrays.js @@ -0,0 +1,243 @@ +/** +* Copyright 2012-2017, 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 Lib = require('../lib'); +var Registry = require('../registry'); + + +var isAddVal = exports.isAddVal = function isAddVal(val) { + return val === 'add' || Lib.isPlainObject(val); +}; + +var isRemoveVal = exports.isRemoveVal = function isRemoveVal(val) { + return val === null || val === 'remove'; +}; + +/* + * containerArrayMatch: does this attribute string point into a + * layout container array? + * + * @param {String} astr: an attribute string, like *annotations[2].text* + * + * @returns {Object | false} Returns false if `astr` doesn't match a container + * array. If it does, returns: + * {array: {String}, index: {Number}, property: {String}} + * ie the attribute string for the array, the index within the array (or '' + * if the whole array) and the property within that (or '' if the whole array + * or the whole object) + */ +exports.containerArrayMatch = function containerArrayMatch(astr) { + var rootContainers = Registry.layoutArrayContainers, + regexpContainers = Registry.layoutArrayRegexes, + rootPart = astr.split('[')[0], + arrayStr, + match; + + // look for regexp matches first, because they may be nested inside root matches + // eg updatemenus[i].buttons is nested inside updatemenus + for(var i = 0; i < regexpContainers.length; i++) { + match = astr.match(regexpContainers[i]); + if(match && match.index === 0) { + arrayStr = match[0]; + break; + } + } + + // now look for root matches + if(!arrayStr) arrayStr = rootContainers[rootContainers.indexOf(rootPart)]; + + if(!arrayStr) return false; + + var tail = astr.substr(arrayStr.length); + if(!tail) return {array: arrayStr, index: '', property: ''}; + + match = tail.match(/^\[(0|[1-9][0-9]*)\](\.(.+))?$/); + if(!match) return false; + + return {array: arrayStr, index: Number(match[1]), property: match[3] || ''}; +}; + +/* + * editContainerArray: for managing arrays of layout components in relayout + * handles them all with a consistent interface. + * + * Here are the supported actions -> relayout calls -> edits we get here + * (as prepared in _relayout): + * + * add an empty obj -> {'annotations[2]': 'add'} -> {2: {'': 'add'}} + * add a specific obj -> {'annotations[2]': {attrs}} -> {2: {'': {attrs}}} + * delete an obj -> {'annotations[2]': 'remove'} -> {2: {'': 'remove'}} + * -> {'annotations[2]': null} -> {2: {'': null}} + * delete the whole array -> {'annotations': 'remove'} -> {'': {'': 'remove'}} + * -> {'annotations': null} -> {'': {'': null}} + * edit an object -> {'annotations[2].text': 'boo'} -> {2: {'text': 'boo'}} + * + * You can combine many edits to different objects. Objects are added and edited + * in ascending order, then removed in descending order. + * For example, starting with [a, b, c], if you want to: + * - replace b with d: + * {'annotations[1]': d, 'annotations[2]': null} (b is item 2 after adding d) + * - add a new item d between a and b, and edit b: + * {'annotations[1]': d, 'annotations[2].x': newX} (b is item 2 after adding d) + * - delete b and edit c: + * {'annotations[1]': null, 'annotations[2].x': newX} (c is edited before b is removed) + * + * You CANNOT combine adding/deleting an item at index `i` with edits to the same index `i` + * You CANNOT combine replacing/deleting the whole array with anything else (for the same array). + * + * @param {HTMLDivElement} gd + * the DOM element of the graph container div + * @param {Lib.nestedProperty} componentType: the array we are editing + * @param {Object} edits + * the changes to make; keys are indices to edit, values are themselves objects: + * {attr: newValue} of changes to make to that index (with add/remove behavior + * in special values of the empty attr) + * @param {Object} flags + * the flags for which actions we're going to perform to display these (and + * any other) changes. If we're already `recalc`ing, we don't need to redraw + * individual items + * + * @returns {bool} `true` if it managed to complete drawing of the changes + * `false` would mean the parent should replot. + */ +exports.editContainerArray = function editContainerArray(gd, np, edits, flags) { + var componentType = np.astr, + supplyComponentDefaults = Registry.getComponentMethod(componentType, 'supplyLayoutDefaults'), + draw = Registry.getComponentMethod(componentType, 'draw'), + drawOne = Registry.getComponentMethod(componentType, 'drawOne'), + replotLater = flags.replot || flags.recalc || (supplyComponentDefaults === Lib.noop) || + (draw === Lib.noop), + layout = gd.layout, + fullLayout = gd._fullLayout; + + if(edits['']) { + if(Object.keys(edits).length > 1) { + Lib.warn('Full array edits are incompatible with other edits', + componentType); + } + + var fullVal = edits['']['']; + + if(isRemoveVal(fullVal)) np.set(null); + else if(Array.isArray(fullVal)) np.set(fullVal); + else { + Lib.warn('Unrecognized full array edit value', componentType, fullVal); + return true; + } + + if(replotLater) return false; + + supplyComponentDefaults(layout, fullLayout); + draw(gd); + return true; + } + + var componentNums = Object.keys(edits).map(Number).sort(), + componentArray = np.get(); + + if(!componentArray) { + componentArray = []; + np.set(componentArray); + } + + var deletes = [], + firstIndexChange = -1, + maxIndex = componentArray.length, + i, + j, + componentNum, + objEdits, + objKeys, + objVal, + adding; + + // first make the add and edit changes + for(i = 0; i < componentNums.length; i++) { + componentNum = componentNums[i]; + objEdits = edits[componentNum]; + objKeys = Object.keys(objEdits); + objVal = objEdits[''], + adding = isAddVal(objVal); + + if(componentNum < 0 || componentNum > componentArray.length - (adding ? 0 : 1)) { + Lib.warn('index out of range', componentType, componentNum); + continue; + } + + if(objVal !== undefined) { + if(objKeys.length > 1) { + Lib.warn( + 'Insertion & removal are incompatible with edits to the same index.', + componentType, componentNum); + } + + if(isRemoveVal(objVal)) { + deletes.push(componentNum); + } + else if(adding) { + if(objVal === 'add') objVal = {}; + componentArray.splice(componentNum, 0, objVal); + } + else { + Lib.warn('Unrecognized full object edit value', + componentType, componentNum, objVal); + } + + if(firstIndexChange === -1) firstIndexChange = componentNum; + } + else { + for(j = 0; j < objKeys.length; j++) { + Lib.nestedProperty(componentArray[componentNum], objKeys[j]).set(objEdits[objKeys[j]]); + } + } + } + + // now do deletes + for(i = deletes.length - 1; i >= 0; i--) { + componentArray.splice(deletes[i], 1); + } + + if(!componentArray.length) np.set(null); + + if(replotLater) return false; + + supplyComponentDefaults(layout, fullLayout); + + // finally draw all the components we need to + // if we added or removed any, redraw all after it + if(drawOne !== Lib.noop) { + var indicesToDraw; + if(firstIndexChange === -1) { + // there's no re-indexing to do, so only redraw components that changed + indicesToDraw = componentNums; + } + else { + // in case the component array was shortened, we still need do call + // drawOne on the latter items so they get properly removed + maxIndex = Math.max(componentArray.length, maxIndex); + indicesToDraw = []; + for(i = 0; i < componentNums.length; i++) { + componentNum = componentNums[i]; + if(componentNum >= firstIndexChange) break; + indicesToDraw.push(componentNum); + } + for(i = firstIndexChange; i < maxIndex; i++) { + indicesToDraw.push(i); + } + } + for(i = 0; i < indicesToDraw.length; i++) { + drawOne(gd, indicesToDraw[i]); + } + } + else draw(gd); + + return true; +}; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 8f6e6803d3b..99b49efcb7f 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -28,6 +28,7 @@ var ErrorBars = require('../components/errorbars'); var xmlnsNamespaces = require('../constants/xmlns_namespaces'); var svgTextUtils = require('../lib/svg_text_utils'); +var manageArrays = require('./manage_arrays'); var helpers = require('./helpers'); var subroutines = require('./subroutines'); @@ -1373,7 +1374,7 @@ function _restyle(gd, aobj, _traces) { return; } // quit if explicitly setting this elsewhere - if(attr in aobj) return; + if(attr in aobj || helpers.hasParent(aobj, attr)) return; var extraparam; if(attr.substr(0, 6) === 'LAYOUT') { @@ -1396,6 +1397,10 @@ function _restyle(gd, aobj, _traces) { // now make the changes to gd.data (and occasionally gd.layout) // and figure out what kind of graphics update we need to do for(var ai in aobj) { + if(helpers.hasParent(aobj, ai)) { + throw new Error('cannot set ' + ai + 'and a parent attribute simultaneously'); + } + var vi = aobj[ai], cont, contFull, @@ -1557,6 +1562,7 @@ function _restyle(gd, aobj, _traces) { helpers.swapXYData(cont); } else if(Plots.dataArrayContainers.indexOf(param.parts[0]) !== -1) { + // TODO: use manageArrays.editContainerArray here too helpers.manageArrayContainers(param, newVal, undoit); flags.docalc = true; } @@ -1717,12 +1723,16 @@ Plotly.relayout = function relayout(gd, astr, val) { if(flags.docalc) gd.calcdata = undefined; // fill in redraw sequence - var seq = []; + + // even if we don't have anything left in aobj, + // something may have happened within relayout that we + // need to wait for + var seq = [Plots.previousPromises]; if(flags.layoutReplot) { seq.push(subroutines.layoutReplot); - } else if(Object.keys(aobj).length) { - seq.push(Plots.previousPromises); + } + else if(Object.keys(aobj).length) { Plots.supplyDefaults(gd); if(flags.dolegend) seq.push(subroutines.doLegend); @@ -1753,13 +1763,16 @@ function _relayout(gd, aobj) { fullLayout = gd._fullLayout, keys = Object.keys(aobj), axes = Plotly.Axes.list(gd), - i; + arrayEdits = {}, + arrayStr, + i, + j; // 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++) { if(keys[i].indexOf('allaxes') === 0) { - for(var j = 0; j < axes.length; j++) { + for(j = 0; j < axes.length; j++) { var scene = axes[j]._id.substr(1), axisAttr = (scene.indexOf('scene') !== -1) ? (scene + '.') : '', newkey = keys[i].replace('allaxes', axisAttr + axes[j]._name); @@ -1797,8 +1810,10 @@ function _relayout(gd, aobj) { attr.forEach(function(a) { doextra(a, val); }); return; } - // quit if explicitly setting this elsewhere - if(attr in aobj) return; + + // if we have another value for this attribute (explicitly or + // via a parent) do not override with this auto-generated extra + if(attr in aobj || helpers.hasParent(aobj, attr)) return; var p = Lib.nestedProperty(layout, attr); if(!(attr in undoit)) undoit[attr] = p.get(); @@ -1807,18 +1822,24 @@ function _relayout(gd, aobj) { // for editing annotations or shapes - is it on autoscaled axes? function refAutorange(obj, axletter) { + if(!Lib.isPlainObject(obj)) return false; var axName = Plotly.Axes.id2name(obj[axletter + 'ref'] || axletter); return (fullLayout[axName] || {}).autorange; } // alter gd.layout for(var ai in aobj) { + if(helpers.hasParent(aobj, ai)) { + throw new Error('cannot set ' + ai + 'and a parent attribute simultaneously'); + } + var p = Lib.nestedProperty(layout, ai), vi = aobj[ai], plen = p.parts.length, // p.parts may end with an index integer if the property is an array pend = typeof p.parts[plen - 1] === 'string' ? (plen - 1) : (plen - 2), // last property in chain (leaf node) + proot = p.parts[0], pleaf = p.parts[pend], // leaf plus immediate parent pleafPlus = p.parts[pend - 1] + '.' + pleaf, @@ -1851,7 +1872,7 @@ function _relayout(gd, aobj) { undefined); } else if(pleafPlus.match(/^aspectratio\.[xyz]$/)) { - doextra(p.parts[0] + '.aspectmode', 'manual'); + doextra(proot + '.aspectmode', 'manual'); } else if(pleafPlus.match(/^aspectmode$/)) { doextra([ptrunk + '.x', ptrunk + '.y', ptrunk + '.z'], undefined); @@ -1876,142 +1897,158 @@ function _relayout(gd, aobj) { 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; + // toggling axis type between log and linear: we need to convert + // positions for components that are still using linearized values, + // not data values like newer components. + // previously we did this for log <-> not-log, but now only do it + // for log <-> linear + if(pleaf === 'type') { + var ax = parentIn, + toLog = parentFull.type === 'linear' && vi === 'log', + fromLog = parentFull.type === 'log' && vi === 'linear'; - if(!ax || !ax.range) { - doextra(ptrunk + '.autorange', true); - } - else if(!parentFull.autorange) { - var r0 = ax.range[0], - r1 = ax.range[1]; - if(vi === 'log') { - // if both limits are negative, autorange - if(r0 <= 0 && r1 <= 0) { - doextra(ptrunk + '.autorange', true); + if(toLog || fromLog) { + if(!ax || !ax.range) { + doextra(ptrunk + '.autorange', true); + } + else if(!parentFull.autorange) { + // toggling log without autorange: need to also recalculate ranges + // because log axes use linearized values for range endpoints + var r0 = ax.range[0], + r1 = ax.range[1]; + if(toLog) { + // if both limits are negative, autorange + if(r0 <= 0 && r1 <= 0) { + doextra(ptrunk + '.autorange', true); + } + // if one is negative, set it 6 orders below the other. + if(r0 <= 0) r0 = r1 / 1e6; + else if(r1 <= 0) r1 = r0 / 1e6; + // now set the range values as appropriate + doextra(ptrunk + '.range[0]', Math.log(r0) / Math.LN10); + doextra(ptrunk + '.range[1]', Math.log(r1) / Math.LN10); + } + else { + doextra(ptrunk + '.range[0]', Math.pow(10, r0)); + doextra(ptrunk + '.range[1]', Math.pow(10, r1)); } - // if one is negative, set it 6 orders below the other. - if(r0 <= 0) r0 = r1 / 1e6; - else if(r1 <= 0) r1 = r0 / 1e6; - // now set the range values as appropriate - doextra(ptrunk + '.range[0]', Math.log(r0) / Math.LN10); - doextra(ptrunk + '.range[1]', Math.log(r1) / Math.LN10); } - else { - doextra(ptrunk + '.range[0]', Math.pow(10, r0)); - doextra(ptrunk + '.range[1]', Math.pow(10, r1)); + else if(toLog) { + // just make sure the range is positive and in the right + // order, it'll get recalculated later + ax.range = (ax.range[1] > ax.range[0]) ? [1, 2] : [2, 1]; } - } - else if(vi === 'log') { - // just make sure the range is positive and in the right - // order, it'll get recalculated later - ax.range = (ax.range[1] > ax.range[0]) ? [1, 2] : [2, 1]; - } - } - // handle axis reversal explicitly, as there's no 'reverse' flag - if(pleaf === 'reverse') { - if(parentIn.range) parentIn.range.reverse(); + // Annotations and images also need to convert to/from linearized coords + // Shapes do not need this :) + Registry.getComponentMethod('annotations', 'convertCoords')(gd, parentFull, vi, doextra); + Registry.getComponentMethod('images', 'convertCoords')(gd, parentFull, vi, doextra); + } else { + // any other type changes: the range from the previous type + // will not make sense, so autorange it. doextra(ptrunk + '.autorange', true); - parentIn.range = [1, 0]; } - - 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 - // that's because add and remove are special - else if(p.parts[0] === 'annotations' || p.parts[0] === 'shapes') { - var objNum = p.parts[1], - objType = p.parts[0], - objList = layout[objType] || [], - obji = objList[objNum] || {}; - - // if p.parts is just an annotation number, and val is either - // 'add' or an entire annotation to add, the undo is 'remove' - // if val is 'remove' then undo is the whole annotation object - if(p.parts.length === 2) { - - // new API, remove annotation / shape with `null` - if(vi === null) aobj[ai] = 'remove'; - - if(aobj[ai] === 'add' || Lib.isPlainObject(aobj[ai])) { - undoit[ai] = 'remove'; + + // alter gd.layout + + // collect array component edits for execution all together + // so we can ensure consistent behavior adding/removing items + // and order-independence for add/remove/edit all together in + // one relayout call + var containerArrayMatch = manageArrays.containerArrayMatch(ai); + if(containerArrayMatch) { + arrayStr = containerArrayMatch.array; + i = containerArrayMatch.index; + var propStr = containerArrayMatch.property, + componentArray = Lib.nestedProperty(layout, arrayStr), + obji = (componentArray || [])[i] || {}; + + if(i === '') { + // replacing the entire array: too much going on, force recalc + flags.docalc = true; + } + else if(propStr === '') { + // special handling of undoit if we're adding or removing an element + // ie 'annotations[2]' which can be {...} (add) or null (remove) + var toggledObj = vi; + if(manageArrays.isAddVal(vi)) { + undoit[ai] = null; } - else if(aobj[ai] === 'remove') { - if(objNum === -1) { - undoit[objType] = objList; - delete undoit[ai]; - } - else undoit[ai] = obji; + else if(manageArrays.isRemoveVal(vi)) { + undoit[ai] = obji; + toggledObj = obji; } - else Lib.log('???', aobj); - } + else Lib.warn('unrecognized full object value', aobj); - if((refAutorange(obji, 'x') || refAutorange(obji, 'y')) && + if(refAutorange(toggledObj, 'x') || refAutorange(toggledObj, 'y')) { + flags.docalc = true; + } + } + else if((refAutorange(obji, 'x') || refAutorange(obji, 'y')) && !Lib.containsAny(ai, ['color', 'opacity', 'align', 'dash'])) { flags.docalc = true; } - // TODO: combine all edits to a given annotation / shape into one call - // as it is we get separate calls for x and y (or ax and ay) on move + // prepare the edits object we'll send to editContainerArray + if(!arrayEdits[arrayStr]) arrayEdits[arrayStr] = {}; + var objEdits = arrayEdits[arrayStr][i]; + if(!objEdits) objEdits = arrayEdits[arrayStr][i] = {}; + objEdits[propStr] = vi; - var drawOne = Registry.getComponentMethod(objType, 'drawOne'); - drawOne(gd, objNum, p.parts.slice(2).join('.'), aobj[ai]); delete aobj[ai]; } - else if( - Plots.layoutArrayContainers.indexOf(p.parts[0]) !== -1 || - (p.parts[0] === 'mapbox' && p.parts[1] === 'layers') - ) { - helpers.manageArrayContainers(p, vi, undoit); - flags.doplot = true; + // handle axis reversal explicitly, as there's no 'reverse' flag + else if(pleaf === 'reverse') { + if(parentIn.range) parentIn.range.reverse(); + else { + doextra(ptrunk + '.autorange', true); + parentIn.range = [1, 0]; + } + + if(parentFull.autorange) flags.docalc = true; + else flags.doplot = true; } - // alter gd.layout else { var pp1 = String(p.parts[1] || ''); // 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) { + if(proot.indexOf('scene') === 0) { if(p.parts[1] === 'camera') flags.docamera = true; else 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(proot.indexOf('geo') === 0) flags.doplot = true; + else if(proot.indexOf('ternary') === 0) flags.doplot = true; else if(ai === 'paper_bgcolor') flags.doplot = true; else if(fullLayout._has('gl2d') && - (ai.indexOf('axis') !== -1 || p.parts[0] === 'plot_bgcolor') + (ai.indexOf('axis') !== -1 || ai === 'plot_bgcolor') ) flags.doplot = true; else if(ai === 'hiddenlabels') flags.docalc = true; - else if(p.parts[0].indexOf('legend') !== -1) flags.dolegend = true; + else if(proot.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(pp1, ['tick', 'exponent', 'grid', 'zeroline'])) { + else if(proot.indexOf('bgcolor') !== -1) flags.dolayoutstyle = true; + else if(plen > 1 && Lib.containsAny(pp1, ['tick', 'exponent', 'grid', 'zeroline'])) { flags.doticks = true; } else if(ai.indexOf('.linewidth') !== -1 && ai.indexOf('axis') !== -1) { flags.doticks = flags.dolayoutstyle = true; } - else if(p.parts.length > 1 && pp1.indexOf('line') !== -1) { + else if(plen > 1 && pp1.indexOf('line') !== -1) { flags.dolayoutstyle = true; } - else if(p.parts.length > 1 && pp1 === 'mirror') { + else if(plen > 1 && pp1 === 'mirror') { flags.doticks = flags.dolayoutstyle = true; } else if(ai === 'margin.pad') { flags.doticks = flags.dolayoutstyle = true; } - else if(p.parts[0] === 'margin' || - p.parts[1] === 'autorange' || - p.parts[1] === 'rangemode' || - p.parts[1] === 'type' || - p.parts[1] === 'domain' || + else if(proot === 'margin' || + pp1 === 'autorange' || + pp1 === 'rangemode' || + pp1 === 'type' || + pp1 === 'domain' || ai.indexOf('calendar') !== -1 || ai.match(/^(bar|box|font)/)) { flags.docalc = true; @@ -2032,6 +2069,13 @@ function _relayout(gd, aobj) { } } + // now we've collected component edits - execute them all together + for(arrayStr in arrayEdits) { + var finished = manageArrays.editContainerArray(gd, + Lib.nestedProperty(layout, arrayStr), arrayEdits[arrayStr], flags); + if(!finished) flags.doplot = true; + } + var oldWidth = gd._fullLayout.width, oldHeight = gd._fullLayout.height; diff --git a/src/plot_api/plot_schema.js b/src/plot_api/plot_schema.js index 2075674108a..5109c15ba24 100644 --- a/src/plot_api/plot_schema.js +++ b/src/plot_api/plot_schema.js @@ -26,8 +26,9 @@ var extendDeep = Lib.extendDeep; var IS_SUBPLOT_OBJ = '_isSubplotObj'; var IS_LINKED_TO_ARRAY = '_isLinkedToArray'; +var ARRAY_ATTR_REGEXPS = '_arrayAttrRegexps'; var DEPRECATED = '_deprecated'; -var UNDERSCORE_ATTRS = [IS_SUBPLOT_OBJ, IS_LINKED_TO_ARRAY, DEPRECATED]; +var UNDERSCORE_ATTRS = [IS_SUBPLOT_OBJ, IS_LINKED_TO_ARRAY, ARRAY_ATTR_REGEXPS, DEPRECATED]; exports.IS_SUBPLOT_OBJ = IS_SUBPLOT_OBJ; exports.IS_LINKED_TO_ARRAY = IS_LINKED_TO_ARRAY; diff --git a/src/plots/mapbox/layout_attributes.js b/src/plots/mapbox/layout_attributes.js index 4df5146acde..0578eaa0029 100644 --- a/src/plots/mapbox/layout_attributes.js +++ b/src/plots/mapbox/layout_attributes.js @@ -16,6 +16,7 @@ var textposition = require('../../traces/scatter/attributes').textposition; module.exports = { + _arrayAttrRegexps: [/^mapbox([2-9]|[1-9][0-9]+)?\.layers/], domain: { x: { valType: 'info_array', diff --git a/src/registry.js b/src/registry.js index 5fb6f2256bd..64ff7c577f0 100644 --- a/src/registry.js +++ b/src/registry.js @@ -19,6 +19,7 @@ exports.subplotsRegistry = {}; exports.transformsRegistry = {}; exports.componentsRegistry = {}; exports.layoutArrayContainers = []; +exports.layoutArrayRegexes = []; /** * register a module as the handler for a trace type @@ -80,6 +81,11 @@ exports.registerSubplot = function(_module) { return; } + // relayout array handling will look for component module methods with this + // name and won't find them because this is a subplot module... but that + // should be fine, it will just fall back on redrawing the plot. + findArrayRegexps(_module); + // not sure what's best for the 'cartesian' type at this point exports.subplotsRegistry[plotType] = _module; }; @@ -89,11 +95,25 @@ exports.registerComponent = function(_module) { exports.componentsRegistry[name] = _module; - if(_module.layoutAttributes && _module.layoutAttributes._isLinkedToArray) { - Lib.pushUnique(exports.layoutArrayContainers, name); + if(_module.layoutAttributes) { + if(_module.layoutAttributes._isLinkedToArray) { + Lib.pushUnique(exports.layoutArrayContainers, name); + } + findArrayRegexps(_module); } }; +function findArrayRegexps(_module) { + if(_module.layoutAttributes) { + var arrayAttrRegexps = _module.layoutAttributes._arrayAttrRegexps; + if(arrayAttrRegexps) { + for(var i = 0; i < arrayAttrRegexps.length; i++) { + Lib.pushUnique(exports.layoutArrayRegexes, arrayAttrRegexps[i]); + } + } + } +} + /** * Get registered module using trace object or trace type * @@ -145,7 +165,8 @@ exports.traceIs = function(traceType, category) { }; /** - * Retrieve component module method + * Retrieve component module method. Falls back on Lib.noop if either the + * module or the method is missing, so the result can always be safely called * * @param {string} name * name of component (as declared in component module) @@ -157,7 +178,7 @@ exports.getComponentMethod = function(name, method) { var _module = exports.componentsRegistry[name]; if(!_module) return Lib.noop; - return _module[method]; + return _module[method] || Lib.noop; }; function getTraceType(traceType) { diff --git a/test/image/mocks/layout_image.json b/test/image/mocks/layout_image.json index 200fb0f9c51..06ef34f1d64 100644 --- a/test/image/mocks/layout_image.json +++ b/test/image/mocks/layout_image.json @@ -11,6 +11,7 @@ } ], "layout": { + "plot_bgcolor": "rgba(0,0,0,0)", "xaxis2": { "anchor": "y2" }, diff --git a/test/jasmine/tests/annotations_test.js b/test/jasmine/tests/annotations_test.js index ee155f9cf0d..bc3d5fc8dd9 100644 --- a/test/jasmine/tests/annotations_test.js +++ b/test/jasmine/tests/annotations_test.js @@ -9,6 +9,7 @@ var d3 = require('d3'); var customMatchers = require('../assets/custom_matchers'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); +var failTest = require('../assets/fail_test'); describe('Test annotations', function() { @@ -144,6 +145,8 @@ describe('annotations relayout', function() { mockLayout = Lib.extendDeep({}, mock.layout); Plotly.plot(gd, mockData, mockLayout).then(done); + + spyOn(Lib, 'warn'); }); afterEach(destroyGraphDiv); @@ -152,12 +155,20 @@ describe('annotations relayout', function() { return d3.selectAll('g.annotation').size(); } + function assertText(index, expected) { + var query = '.annotation[data-index="' + index + '"]', + actual = d3.select(query).select('text').text(); + + expect(actual).toEqual(expected); + } + it('should be able to add /remove annotations', function(done) { expect(countAnnotations()).toEqual(len); var ann = { text: '' }; - Plotly.relayout(gd, 'annotations[' + len + ']', ann).then(function() { + Plotly.relayout(gd, 'annotations[' + len + ']', ann) + .then(function() { expect(countAnnotations()).toEqual(len + 1); return Plotly.relayout(gd, 'annotations[0]', 'remove'); @@ -175,25 +186,29 @@ describe('annotations relayout', function() { .then(function() { expect(countAnnotations()).toEqual(len - 2); - return Plotly.relayout(gd, { annotations: [] }); + return Plotly.relayout(gd, {annotations: []}); }) .then(function() { expect(countAnnotations()).toEqual(0); - done(); - }); + return Plotly.relayout(gd, {annotations: [ann, {text: 'boo', x: 1, y: 1}]}); + }) + .then(function() { + expect(countAnnotations()).toEqual(2); + + return Plotly.relayout(gd, {annotations: null}); + }) + .then(function() { + expect(countAnnotations()).toEqual(0); + expect(Lib.warn).not.toHaveBeenCalled(); + }) + .catch(failTest) + .then(done); }); it('should be able update annotations', function(done) { var updateObj = { 'annotations[0].text': 'hello' }; - function assertText(index, expected) { - var query = '.annotation[data-index="' + index + '"]', - actual = d3.select(query).select('text').text(); - - expect(actual).toEqual(expected); - } - function assertUpdateObj() { // w/o mutating relayout update object expect(Object.keys(updateObj)).toEqual(['annotations[0].text']); @@ -227,8 +242,235 @@ describe('annotations relayout', function() { assertText(0, 'hello'); assertUpdateObj(); }) + .catch(failTest) + .then(done); + + }); + + it('can update several annotations and add and delete in one call', function(done) { + expect(countAnnotations()).toEqual(len); + var annos = gd.layout.annotations, + anno0 = Lib.extendFlat(annos[0]), + anno1 = Lib.extendFlat(annos[1]), + anno3 = Lib.extendFlat(annos[3]); + + Plotly.relayout(gd, { + 'annotations[0].text': 'tortilla', + 'annotations[0].x': 3.45, + 'annotations[1]': {text: 'chips', x: 1.1, y: 2.2}, // add new annotation btwn 0 and 1 + 'annotations[2].text': 'guacamole', // alter 2 (which was 1 before we started) + 'annotations[3]': null, // remove 3 (which was 2 before we started) + 'annotations[4].text': 'lime' // alter 4 (which was 3 before and will be 3 afterward) + }) + .then(function() { + expect(countAnnotations()).toEqual(len); + + assertText(0, 'tortilla'); + anno0.text = 'tortilla'; + expect(annos[0]).toEqual(anno0); + + assertText(1, 'chips'); + expect(annos[1]).toEqual({text: 'chips', x: 1.1, y: 2.2}); + + assertText(2, 'guacamole'); + anno1.text = 'guacamole'; + expect(annos[2]).toEqual(anno1); + + assertText(3, 'lime'); + anno3.text = 'lime'; + expect(annos[3]).toEqual(anno3); + expect(Lib.warn).not.toHaveBeenCalled(); + }) + .catch(failTest) + .then(done); + }); + + [ + {annotations: [{text: 'a'}], 'annotations[0]': {text: 'b'}}, + {annotations: null, 'annotations[0]': {text: 'b'}}, + {annotations: [{text: 'a'}], 'annotations[0]': null}, + {annotations: [{text: 'a'}], 'annotations[0].text': 'b'}, + {'annotations[0]': {text: 'a'}, 'annotations[0].text': 'b'}, + {'annotations[0]': null, 'annotations[0].text': 'b'}, + {annotations: {text: 'a'}}, + {'annotations[0]': 'not an object'}, + {'annotations[100]': {text: 'bad index'}} + ].forEach(function(update) { + it('warns on ambiguous combinations and invalid values: ' + JSON.stringify(update), function() { + Plotly.relayout(gd, update); + expect(Lib.warn).toHaveBeenCalled(); + // we could test the results here, but they're ambiguous and/or undefined so why bother? + // the important thing is the developer is warned that something went wrong. + }); + }); + + it('handles xref/yref changes with or without x/y changes', function(done) { + Plotly.relayout(gd, { + + // #0: change all 4, with opposite ordering of keys + 'annotations[0].x': 2.2, + 'annotations[0].xref': 'x', + 'annotations[0].yref': 'y', + 'annotations[0].y': 3.3, + + // #1: change xref and yref without x and y: no longer changes x & y + 'annotations[1].xref': 'x', + 'annotations[1].yref': 'y', + + // #2: change x and y + 'annotations[2].x': 0.1, + 'annotations[2].y': 0.3 + }) + .then(function() { + var annos = gd.layout.annotations; + + // explicitly change all 4 + expect(annos[0].x).toBe(2.2); + expect(annos[0].y).toBe(3.3); + expect(annos[0].xref).toBe('x'); + expect(annos[0].yref).toBe('y'); + + // just change xref/yref -> we do NOT make any implicit changes + // to x/y within plotly.js + expect(annos[1].x).toBe(0.25); + expect(annos[1].y).toBe(1); + expect(annos[1].xref).toBe('x'); + expect(annos[1].yref).toBe('y'); + + // just change x/y -> nothing else changes + expect(annos[2].x).toBe(0.1); + expect(annos[2].y).toBe(0.3); + expect(annos[2].xref).toBe('paper'); + expect(annos[2].yref).toBe('paper'); + expect(Lib.warn).not.toHaveBeenCalled(); + }) + .catch(failTest) + .then(done); + }); +}); + +describe('annotations log/linear axis changes', function() { + 'use strict'; + + var mock = { + data: [ + {x: [1, 2, 3], y: [1, 2, 3]}, + {x: [1, 2, 3], y: [3, 2, 1], yaxis: 'y2'} + ], + layout: { + annotations: [ + {x: 1, y: 1, text: 'boo', xref: 'x', yref: 'y'}, + {x: 1, y: 1, text: '', ax: 2, ay: 2, axref: 'x', ayref: 'y'} + ], + yaxis: {range: [1, 3]}, + yaxis2: {range: [0, 1], overlaying: 'y', type: 'log'} + } + }; + var gd; + + beforeEach(function(done) { + gd = createGraphDiv(); + + var mockData = Lib.extendDeep([], mock.data), + mockLayout = Lib.extendDeep({}, mock.layout); + + Plotly.plot(gd, mockData, mockLayout).then(done); + }); + + afterEach(destroyGraphDiv); + + it('doesnt try to update position automatically with ref changes', function(done) { + // we don't try to figure out the position on a new axis / canvas + // automatically when you change xref / yref, we leave it to the caller. + // previously this logic was part of plotly.js... But it's really only + // the plot.ly workspace that wants this and can assign an unambiguous + // meaning to it, so we'll move the logic there, where there are far + // fewer edge cases to consider because xref never gets edited along + // with anything else in one `relayout` call. + + // linear to log + Plotly.relayout(gd, {'annotations[0].yref': 'y2'}) + .then(function() { + expect(gd.layout.annotations[0].y).toBe(1); + + // log to paper + return Plotly.relayout(gd, {'annotations[0].yref': 'paper'}); + }) + .then(function() { + expect(gd.layout.annotations[0].y).toBe(1); + + // paper to log + return Plotly.relayout(gd, {'annotations[0].yref': 'y2'}); + }) + .then(function() { + expect(gd.layout.annotations[0].y).toBe(1); + + // log to linear + return Plotly.relayout(gd, {'annotations[0].yref': 'y'}); + }) + .then(function() { + expect(gd.layout.annotations[0].y).toBe(1); + + // y and yref together + return Plotly.relayout(gd, {'annotations[0].y': 0.2, 'annotations[0].yref': 'y2'}); + }) + .then(function() { + expect(gd.layout.annotations[0].y).toBe(0.2); + + // yref first, then y + return Plotly.relayout(gd, {'annotations[0].yref': 'y', 'annotations[0].y': 2}); + }) + .then(function() { + expect(gd.layout.annotations[0].y).toBe(2); + }) + .catch(failTest) .then(done); }); + + it('keeps the same data value if the axis type is changed without position', function(done) { + // because annotations (and images) use linearized positions on log axes, + // we have `relayout` update the positions so the data value the annotation + // points to is unchanged by the axis type change. + + Plotly.relayout(gd, {'yaxis.type': 'log'}) + .then(function() { + expect(gd.layout.annotations[0].y).toBe(0); + expect(gd.layout.annotations[1].y).toBe(0); + expect(gd.layout.annotations[1].ay).toBeCloseTo(Math.LN2 / Math.LN10, 6); + + return Plotly.relayout(gd, {'yaxis.type': 'linear'}); + }) + .then(function() { + expect(gd.layout.annotations[0].y).toBe(1); + expect(gd.layout.annotations[1].y).toBe(1); + expect(gd.layout.annotations[1].ay).toBeCloseTo(2, 6); + + return Plotly.relayout(gd, { + 'yaxis.type': 'log', + 'annotations[0].y': 0.2, + 'annotations[1].ay': 0.3 + }); + }) + .then(function() { + expect(gd.layout.annotations[0].y).toBe(0.2); + expect(gd.layout.annotations[1].y).toBe(0); + expect(gd.layout.annotations[1].ay).toBe(0.3); + + return Plotly.relayout(gd, { + 'annotations[0].y': 2, + 'annotations[1].ay': 2.5, + 'yaxis.type': 'linear' + }); + }) + .then(function() { + expect(gd.layout.annotations[0].y).toBe(2); + expect(gd.layout.annotations[1].y).toBe(1); + expect(gd.layout.annotations[1].ay).toBe(2.5); + }) + .catch(failTest) + .then(done); + }); + }); describe('annotations autosize', function() { @@ -317,6 +559,7 @@ describe('annotations autosize', function() { [0.9, 2.1], [0.86, 2.14] ); }) + .catch(failTest) .then(done); }); }); @@ -445,6 +688,7 @@ describe('annotation clicktoshow', function() { // finally click each one off .then(clickAndCheck({newPts: [[1, 2]], newCTS: true, on: [2], step: 15})) .then(clickAndCheck({newPts: [[2, 3]], newCTS: true, on: [], step: 16})) + .catch(failTest) .then(done); }); }); diff --git a/test/jasmine/tests/layout_images_test.js b/test/jasmine/tests/layout_images_test.js index 71d7c2c8692..e087d6f97c7 100644 --- a/test/jasmine/tests/layout_images_test.js +++ b/test/jasmine/tests/layout_images_test.js @@ -1,10 +1,12 @@ var Plotly = require('@lib/index'); var Plots = require('@src/plots/plots'); var Images = require('@src/components/images'); +var Lib = require('@src/lib'); var d3 = require('d3'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); +var failTest = require('../assets/fail_test'); var mouseEvent = require('../assets/mouse_event'); var jsLogo = 'https://images.plot.ly/language-icons/api-home/js-logo.png'; @@ -337,7 +339,8 @@ describe('Layout images', function() { .then(function() { assertImages(2); - return Plotly.relayout(gd, 'images[2]', makeImage(pythonLogo, 0.2, 0.5)); + // insert an image not at the end of the array + return Plotly.relayout(gd, 'images[0]', makeImage(pythonLogo, 0.2, 0.5)); }) .then(function() { assertImages(3); @@ -355,7 +358,8 @@ describe('Layout images', function() { assertImages(3); expect(gd.layout.images.length).toEqual(3); - return Plotly.relayout(gd, 'images[2]', null); + // delete not from the end of the array + return Plotly.relayout(gd, 'images[0]', null); }) .then(function() { assertImages(2); @@ -380,3 +384,122 @@ describe('Layout images', function() { }); }); + +describe('images log/linear axis changes', function() { + 'use strict'; + + var mock = { + data: [ + {x: [1, 2, 3], y: [1, 2, 3]}, + {x: [1, 2, 3], y: [3, 2, 1], yaxis: 'y2'} + ], + layout: { + images: [{ + source: 'https://images.plot.ly/language-icons/api-home/python-logo.png', + x: 1, + y: 1, + xref: 'x', + yref: 'y', + sizex: 2, + sizey: 2 + }], + yaxis: {range: [1, 3]}, + yaxis2: {range: [0, 1], overlaying: 'y', type: 'log'} + } + }; + var gd; + + beforeEach(function(done) { + gd = createGraphDiv(); + + var mockData = Lib.extendDeep([], mock.data), + mockLayout = Lib.extendDeep({}, mock.layout); + + Plotly.plot(gd, mockData, mockLayout).then(done); + }); + + afterEach(destroyGraphDiv); + + it('doesnt try to update position automatically with ref changes', function(done) { + // we don't try to figure out the position on a new axis / canvas + // automatically when you change xref / yref, we leave it to the caller. + + // linear to log + Plotly.relayout(gd, {'images[0].yref': 'y2'}) + .then(function() { + expect(gd.layout.images[0].y).toBe(1); + + // log to paper + return Plotly.relayout(gd, {'images[0].yref': 'paper'}); + }) + .then(function() { + expect(gd.layout.images[0].y).toBe(1); + + // paper to log + return Plotly.relayout(gd, {'images[0].yref': 'y2'}); + }) + .then(function() { + expect(gd.layout.images[0].y).toBe(1); + + // log to linear + return Plotly.relayout(gd, {'images[0].yref': 'y'}); + }) + .then(function() { + expect(gd.layout.images[0].y).toBe(1); + + // y and yref together + return Plotly.relayout(gd, {'images[0].y': 0.2, 'images[0].yref': 'y2'}); + }) + .then(function() { + expect(gd.layout.images[0].y).toBe(0.2); + + // yref first, then y + return Plotly.relayout(gd, {'images[0].yref': 'y', 'images[0].y': 2}); + }) + .then(function() { + expect(gd.layout.images[0].y).toBe(2); + }) + .catch(failTest) + .then(done); + }); + + it('keeps the same data value if the axis type is changed without position', function(done) { + // because images (and images) use linearized positions on log axes, + // we have `relayout` update the positions so the data value the annotation + // points to is unchanged by the axis type change. + + Plotly.relayout(gd, {'yaxis.type': 'log'}) + .then(function() { + expect(gd.layout.images[0].y).toBe(0); + expect(gd.layout.images[0].sizey).toBeCloseTo(0.765551370675726, 6); + + return Plotly.relayout(gd, {'yaxis.type': 'linear'}); + }) + .then(function() { + expect(gd.layout.images[0].y).toBe(1); + expect(gd.layout.images[0].sizey).toBeCloseTo(2, 6); + + return Plotly.relayout(gd, { + 'yaxis.type': 'log', + 'images[0].y': 0.2, + 'images[0].sizey': 0.3 + }); + }) + .then(function() { + expect(gd.layout.images[0].y).toBe(0.2); + expect(gd.layout.images[0].sizey).toBe(0.3); + + return Plotly.relayout(gd, { + 'images[0].y': 2, + 'images[0].sizey': 2.5, + 'yaxis.type': 'linear' + }); + }) + .then(function() { + expect(gd.layout.images[0].y).toBe(2); + expect(gd.layout.images[0].sizey).toBe(2.5); + }) + .catch(failTest) + .then(done); + }); +}); diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index 7181510d4cc..189a3835cf9 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -10,6 +10,7 @@ var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var Plots = PlotlyInternal.Plots; var customMatchers = require('../assets/custom_matchers'); +var failTest = require('../assets/fail_test'); describe('Test lib.js:', function() { 'use strict'; @@ -292,7 +293,7 @@ describe('Test lib.js:', function() { prop.set(null); expect(prop.get()).toBe(undefined); - expect(obj).toEqual({arr: [undefined, undefined, {b: 3}]}); + expect(obj).toEqual({arr: [{}, {}, {b: 3}]}); prop.set([2, 3, 4]); expect(prop.get()).toEqual([2, 3, 4]); @@ -329,7 +330,7 @@ describe('Test lib.js:', function() { expect(obj).toEqual({a: false, b: '', c: 0, d: NaN}); }); - it('should remove containers but not data arrays', function() { + it('should not remove data arrays or empty objects inside container arrays', function() { var obj = { annotations: [{a: [1, 2, 3]}], c: [1, 2, 3], @@ -344,23 +345,23 @@ describe('Test lib.js:', function() { propR = np(obj, 'range'), propS = np(obj, 'shapes[0]'); - propA.set([]); + propA.set([[]]); propC.set([]); propD0.set(undefined); propD1.set(undefined); propR.set([]); propS.set(null); - expect(obj).toEqual({c: []}); + // 'a' and 'c' are both potentially data arrays so we need to keep them + expect(obj).toEqual({annotations: [{a: []}], c: []}); }); - it('should have no empty object sub-containers but contain empty data arrays', function() { + it('should allow empty object sub-containers only in arrays', function() { var obj = {}, prop = np(obj, 'a[1].b.c'), - expectedArr = []; - - expectedArr[1] = {b: {c: 'pizza'}}; + // we never set a value into a[0] so it doesn't even get {} + expectedArr = [undefined, {b: {c: 'pizza'}}]; expect(prop.get()).toBe(undefined); expect(obj).toEqual({}); @@ -371,7 +372,7 @@ describe('Test lib.js:', function() { prop.set(null); expect(prop.get()).toBe(undefined); - expect(obj).toEqual({a: []}); + expect(obj).toEqual({a: [undefined, {}]}); }); it('should get empty, and fail on set, with a bad input object', function() { @@ -1282,6 +1283,18 @@ describe('Test lib.js:', function() { expect(this.array).toEqual(['a', 'b', 'c', { a: 'A' }]); }); + + it('should recognize matching RegExps', function() { + expect(this.array).toEqual(['a', 'b', 'c', { a: 'A' }]); + + var r1 = /a/, + r2 = /a/; + Lib.pushUnique(this.array, r1); + expect(this.array).toEqual(['a', 'b', 'c', { a: 'A' }, r1]); + + Lib.pushUnique(this.array, r2); + expect(this.array).toEqual(['a', 'b', 'c', { a: 'A' }, r1]); + }); }); describe('filterUnique', function() { @@ -1652,8 +1665,9 @@ describe('Queue', function() { return Plotly.relayout(gd, 'updatemenus[0]', null); }) .then(function() { + // buttons have been stripped out because it's an empty container array... expect(gd.undoQueue.queue[1].undo.args[0][1]) - .toEqual({ 'updatemenus[0]': { buttons: []} }); + .toEqual({ 'updatemenus[0]': {} }); expect(gd.undoQueue.queue[1].redo.args[0][1]) .toEqual({ 'updatemenus[0]': null }); @@ -1664,8 +1678,8 @@ describe('Queue', function() { .toEqual({ 'transforms[0]': [ { type: 'filter' } ]}); expect(gd.undoQueue.queue[1].redo.args[0][1]) .toEqual({ 'transforms[0]': null }); - - done(); - }); + }) + .catch(failTest) + .then(done); }); }); diff --git a/test/jasmine/tests/mapbox_test.js b/test/jasmine/tests/mapbox_test.js index b9146242e3c..b397def52be 100644 --- a/test/jasmine/tests/mapbox_test.js +++ b/test/jasmine/tests/mapbox_test.js @@ -10,6 +10,7 @@ var destroyGraphDiv = require('../assets/destroy_graph_div'); var hasWebGLSupport = require('../assets/has_webgl_support'); var mouseEvent = require('../assets/mouse_event'); var customMatchers = require('../assets/custom_matchers'); +var failTest = require('../assets/fail_test'); var MAPBOX_ACCESS_TOKEN = require('@build/credentials.json').MAPBOX_ACCESS_TOKEN; var TRANSITION_DELAY = 500; @@ -504,9 +505,9 @@ describe('mapbox plots', function() { expect(relayoutCnt).toEqual(5); assertLayout('Mapbox Light', [0, 0], 6, [80, 100, 454, 135]); - - done(); - }); + }) + .catch(failTest) + .then(done); }, LONG_TIMEOUT_INTERVAL); it('should be able to add, update and remove layers', function(done) { @@ -567,6 +568,7 @@ describe('mapbox plots', function() { expect(gd.layout.mapbox.layers.length).toEqual(1); expect(countVisibleLayers(gd)).toEqual(1); + // add a new layer at the beginning return Plotly.relayout(gd, 'mapbox.layers[1]', layer1); }) .then(function() { @@ -611,7 +613,8 @@ describe('mapbox plots', function() { expect(gd.layout.mapbox.layers.length).toEqual(2); expect(countVisibleLayers(gd)).toEqual(2); - return Plotly.relayout(gd, 'mapbox.layers[1]', null); + // delete the first layer + return Plotly.relayout(gd, 'mapbox.layers[0]', null); }) .then(function() { expect(gd.layout.mapbox.layers.length).toEqual(1); @@ -620,13 +623,13 @@ describe('mapbox plots', function() { return Plotly.relayout(gd, 'mapbox.layers[0]', null); }) .then(function() { - expect(gd.layout.mapbox.layers.length).toEqual(0); + expect(gd.layout.mapbox.layers).toBeUndefined(); expect(countVisibleLayers(gd)).toEqual(0); return Plotly.relayout(gd, 'mapbox.layers[0]', {}); }) .then(function() { - expect(gd.layout.mapbox.layers).toEqual([]); + expect(gd.layout.mapbox.layers).toEqual([{}]); expect(countVisibleLayers(gd)).toEqual(0); // layer with no source are not drawn @@ -636,9 +639,9 @@ describe('mapbox plots', function() { .then(function() { expect(gd.layout.mapbox.layers.length).toEqual(1); expect(countVisibleLayers(gd)).toEqual(1); - - done(); - }); + }) + .catch(failTest) + .then(done); }, LONG_TIMEOUT_INTERVAL); it('should be able to update the access token', function(done) { @@ -651,8 +654,9 @@ describe('mapbox plots', function() { }).then(function() { expect(gd._fullLayout.mapbox.accesstoken).toEqual(MAPBOX_ACCESS_TOKEN); expect(gd._promises.length).toEqual(0); - done(); - }); + }) + .catch(failTest) + .then(done); }, LONG_TIMEOUT_INTERVAL); it('should be able to update traces', function(done) { @@ -688,9 +692,9 @@ describe('mapbox plots', function() { }) .then(function() { assertDataPts([5, 5]); - - done(); - }); + }) + .catch(failTest) + .then(done); }, LONG_TIMEOUT_INTERVAL); it('should display to hover labels on mouse over', function(done) { @@ -704,7 +708,9 @@ describe('mapbox plots', function() { assertMouseMove(blankPos, 0).then(function() { return assertMouseMove(pointPos, 1); - }).then(done); + }) + .catch(failTest) + .then(done); }, LONG_TIMEOUT_INTERVAL); it('should respond to hover interactions by', function(done) { @@ -750,9 +756,9 @@ describe('mapbox plots', function() { .then(function() { expect(hoverCnt).toEqual(1); expect(unhoverCnt).toEqual(1); - - done(); - }); + }) + .catch(failTest) + .then(done); }, LONG_TIMEOUT_INTERVAL); it('should respond drag / scroll interactions', function(done) { @@ -808,6 +814,7 @@ describe('mapbox plots', function() { assertLayout([-19.651, 13.751], 1.234, { withUpdateData: true }); }) + .catch(failTest) .then(done); // TODO test scroll @@ -844,6 +851,7 @@ describe('mapbox plots', function() { expect(ptData.pointNumber).toEqual(0, 'returning the correct point number'); }); }) + .catch(failTest) .then(done); }, LONG_TIMEOUT_INTERVAL); diff --git a/test/jasmine/tests/shapes_test.js b/test/jasmine/tests/shapes_test.js index 43714ebd8df..bfd3ed490b2 100644 --- a/test/jasmine/tests/shapes_test.js +++ b/test/jasmine/tests/shapes_test.js @@ -13,6 +13,7 @@ var d3 = require('d3'); var customMatchers = require('../assets/custom_matchers'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); +var failTest = require('../assets/fail_test'); describe('Test shapes defaults:', function() { @@ -186,7 +187,9 @@ describe('Test shapes:', function() { expect(countShapeLowerLayerNodes()).toEqual(1); expect(countShapePathsInLowerLayer()) .toEqual(countShapesInLowerLayer()); - }).then(done); + }) + .catch(failTest) + .then(done); }); }); @@ -205,7 +208,9 @@ describe('Test shapes:', function() { expect(countShapeUpperLayerNodes()).toEqual(1); expect(countShapePathsInUpperLayer()) .toEqual(countShapesInUpperLayer()); - }).then(done); + }) + .catch(failTest) + .then(done); }); }); @@ -226,7 +231,9 @@ describe('Test shapes:', function() { .toEqual(countSubplots(gd)); expect(countShapePathsInSubplots()) .toEqual(countShapesInSubplots()); - }).then(done); + }) + .catch(failTest) + .then(done); }); }); @@ -261,7 +268,17 @@ describe('Test shapes:', function() { expect(countShapePathsInUpperLayer()).toEqual(pathCount + 1); expect(getLastShape(gd)).toEqual(shape); expect(countShapes(gd)).toEqual(index + 1); - }).then(done); + + // add a shape not at the end of the array + return Plotly.relayout(gd, 'shapes[0]', getRandomShape()); + }) + .then(function() { + expect(countShapePathsInUpperLayer()).toEqual(pathCount + 2); + expect(getLastShape(gd)).toEqual(shape); + expect(countShapes(gd)).toEqual(index + 2); + }) + .catch(failTest) + .then(done); }); it('should be able to remove a shape', function(done) { @@ -292,15 +309,32 @@ describe('Test shapes:', function() { expect(countShapePathsInUpperLayer()).toEqual(pathCount - 2); expect(countShapes(gd)).toEqual(index - 1); }) + .catch(failTest) .then(done); }); it('should be able to remove all shapes', function(done) { - Plotly.relayout(gd, { shapes: [] }).then(function() { + Plotly.relayout(gd, { shapes: null }).then(function() { expect(countShapePathsInUpperLayer()).toEqual(0); expect(countShapePathsInLowerLayer()).toEqual(0); expect(countShapePathsInSubplots()).toEqual(0); - }).then(done); + }) + .catch(failTest) + .then(done); + }); + + it('can replace the shapes array', function(done) { + Plotly.relayout(gd, { shapes: [ + getRandomShape(), + getRandomShape() + ]}).then(function() { + expect(countShapePathsInUpperLayer()).toEqual(2); + expect(countShapePathsInLowerLayer()).toEqual(0); + expect(countShapePathsInSubplots()).toEqual(0); + expect(gd.layout.shapes.length).toBe(2); + }) + .catch(failTest) + .then(done); }); it('should be able to update a shape layer', function(done) { @@ -340,7 +374,9 @@ describe('Test shapes:', function() { .toEqual(shapesInUpperLayer + 1); expect(getLastShape(gd)).toEqual(shape); expect(countShapes(gd)).toEqual(index + 1); - }).then(done); + }) + .catch(failTest) + .then(done); }); }); }); @@ -404,6 +440,7 @@ describe('shapes autosize', function() { .then(function() { assertRanges([0, 3], [0, 2]); }) + .catch(failTest) .then(done); }); }); @@ -450,7 +487,9 @@ describe('Test shapes: a plot with shapes and an overlaid axis', function() { afterEach(destroyGraphDiv); it('should not throw an exception', function(done) { - Plotly.plot(gd, data, layout).then(done); + Plotly.plot(gd, data, layout) + .catch(failTest) + .then(done); }); }); From a9526bf1d825a3f900f1f02dd90c8433dbbaf7a2 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 21 Feb 2017 08:34:02 -0500 Subject: [PATCH 04/18] test fixin --- src/plot_api/manage_arrays.js | 9 +++----- src/plot_api/plot_api.js | 19 ++++++++++++++--- test/jasmine/tests/command_test.js | 3 ++- test/jasmine/tests/layout_images_test.js | 3 ++- test/jasmine/tests/mapbox_test.js | 27 ++++++++++++++---------- test/jasmine/tests/plotschema_test.js | 5 ++++- test/jasmine/tests/updatemenus_test.js | 5 +++-- 7 files changed, 46 insertions(+), 25 deletions(-) diff --git a/src/plot_api/manage_arrays.js b/src/plot_api/manage_arrays.js index eff0d6284ab..1bd76dbe940 100644 --- a/src/plot_api/manage_arrays.js +++ b/src/plot_api/manage_arrays.js @@ -141,12 +141,8 @@ exports.editContainerArray = function editContainerArray(gd, np, edits, flags) { } var componentNums = Object.keys(edits).map(Number).sort(), - componentArray = np.get(); - - if(!componentArray) { - componentArray = []; - np.set(componentArray); - } + componentArrayIn = np.get(), + componentArray = componentArrayIn || []; var deletes = [], firstIndexChange = -1, @@ -206,6 +202,7 @@ exports.editContainerArray = function editContainerArray(gd, np, edits, flags) { } if(!componentArray.length) np.set(null); + else if(!componentArrayIn) np.set(componentArray); if(replotLater) return false; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 99b49efcb7f..428d80f4206 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -31,6 +31,7 @@ var svgTextUtils = require('../lib/svg_text_utils'); var manageArrays = require('./manage_arrays'); var helpers = require('./helpers'); var subroutines = require('./subroutines'); +var cartesianConstants = require('../plots/cartesian/constants'); /** @@ -1950,6 +1951,17 @@ function _relayout(gd, aobj) { doextra(ptrunk + '.autorange', true); } } + else if(pleaf.match(cartesianConstants.AX_NAME_PATTERN)) { + var fullProp = Lib.nestedProperty(fullLayout, ai).get(), + newType = (vi || {}).type; + + // This can potentially cause strange behavior if the autotype is not + // numeric (linear, because we don't auto-log) but the previous type + // was log. That's a very strange edge case though + if(!newType || newType === '-') newType = 'linear'; + Registry.getComponentMethod('annotations', 'convertCoords')(gd, fullProp, newType, doextra); + Registry.getComponentMethod('images', 'convertCoords')(gd, fullProp, newType, doextra); + } // alter gd.layout @@ -1967,7 +1979,7 @@ function _relayout(gd, aobj) { if(i === '') { // replacing the entire array: too much going on, force recalc - flags.docalc = true; + if(ai.indexOf('updatemenus') === -1) flags.docalc = true; } else if(propStr === '') { // special handling of undoit if we're adding or removing an element @@ -1982,12 +1994,13 @@ function _relayout(gd, aobj) { } else Lib.warn('unrecognized full object value', aobj); - if(refAutorange(toggledObj, 'x') || refAutorange(toggledObj, 'y')) { + if(refAutorange(toggledObj, 'x') || refAutorange(toggledObj, 'y') && + ai.indexOf('updatemenus') === -1) { flags.docalc = true; } } else if((refAutorange(obji, 'x') || refAutorange(obji, 'y')) && - !Lib.containsAny(ai, ['color', 'opacity', 'align', 'dash'])) { + !Lib.containsAny(ai, ['color', 'opacity', 'align', 'dash', 'updatemenus'])) { flags.docalc = true; } diff --git a/test/jasmine/tests/command_test.js b/test/jasmine/tests/command_test.js index 808fe17dfc5..6d334dbb51c 100644 --- a/test/jasmine/tests/command_test.js +++ b/test/jasmine/tests/command_test.js @@ -594,7 +594,8 @@ describe('attaching component bindings', function() { expect(gd.layout.sliders[0].active).toBe(0); // Check that it still has one attached listener: - expect(typeof gd._internalEv._events.plotly_animatingframe).toBe('function'); + expect(typeof gd._internalEv._events.plotly_animatingframe).toBe('function', + gd._internalEv._events.plotly_animatingframe); // Change this to a non-simple binding: return Plotly.relayout(gd, {'sliders[0].steps[0].args[0]': 'line.color'}); diff --git a/test/jasmine/tests/layout_images_test.js b/test/jasmine/tests/layout_images_test.js index e087d6f97c7..e38cab663f7 100644 --- a/test/jasmine/tests/layout_images_test.js +++ b/test/jasmine/tests/layout_images_test.js @@ -328,6 +328,7 @@ describe('Layout images', function() { Plotly.plot(gd, data, layout).then(function() { assertImages(0); + expect(gd.layout.images).toBeUndefined(); return Plotly.relayout(gd, 'images[0]', makeImage(jsLogo, 0.1, 0.1)); }) @@ -375,7 +376,7 @@ describe('Layout images', function() { }) .then(function() { assertImages(0); - expect(gd.layout.images).toEqual([]); + expect(gd.layout.images).toBeUndefined(); done(); }); diff --git a/test/jasmine/tests/mapbox_test.js b/test/jasmine/tests/mapbox_test.js index b397def52be..61a7e868a9b 100644 --- a/test/jasmine/tests/mapbox_test.js +++ b/test/jasmine/tests/mapbox_test.js @@ -545,17 +545,22 @@ describe('mapbox plots', function() { return layerLen; } + function getLayerLength(gd) { + return (gd.layout.mapbox.layers || []).length; + } + function assertLayerStyle(gd, expectations, index) { var mapInfo = getMapInfo(gd), layers = mapInfo.layers, layerNames = mapInfo.layoutLayers; var layer = layers[layerNames[index]]; + expect(layer).toBeDefined(layerNames[index]); return new Promise(function(resolve) { setTimeout(function() { Object.keys(expectations).forEach(function(k) { - expect(layer.paint[k]).toEqual(expectations[k]); + expect(((layer || {}).paint || {})[k]).toEqual(expectations[k]); }); resolve(); }, TRANSITION_DELAY); @@ -565,26 +570,26 @@ describe('mapbox plots', function() { expect(countVisibleLayers(gd)).toEqual(0); Plotly.relayout(gd, 'mapbox.layers[0]', layer0).then(function() { - expect(gd.layout.mapbox.layers.length).toEqual(1); + expect(getLayerLength(gd)).toEqual(1); expect(countVisibleLayers(gd)).toEqual(1); // add a new layer at the beginning return Plotly.relayout(gd, 'mapbox.layers[1]', layer1); }) .then(function() { - expect(gd.layout.mapbox.layers.length).toEqual(2); + expect(getLayerLength(gd)).toEqual(2); expect(countVisibleLayers(gd)).toEqual(2); return Plotly.relayout(gd, mapUpdate); }) .then(function() { - expect(gd.layout.mapbox.layers.length).toEqual(2); + expect(getLayerLength(gd)).toEqual(2); expect(countVisibleLayers(gd)).toEqual(2); return Plotly.relayout(gd, styleUpdate0); }) .then(function() { - expect(gd.layout.mapbox.layers.length).toEqual(2); + expect(getLayerLength(gd)).toEqual(2); expect(countVisibleLayers(gd)).toEqual(2); return assertLayerStyle(gd, { @@ -594,13 +599,13 @@ describe('mapbox plots', function() { }, 0); }) .then(function() { - expect(gd.layout.mapbox.layers.length).toEqual(2); + expect(getLayerLength(gd)).toEqual(2); expect(countVisibleLayers(gd)).toEqual(2); return Plotly.relayout(gd, styleUpdate1); }) .then(function() { - expect(gd.layout.mapbox.layers.length).toEqual(2); + expect(getLayerLength(gd)).toEqual(2); expect(countVisibleLayers(gd)).toEqual(2); return assertLayerStyle(gd, { @@ -610,20 +615,20 @@ describe('mapbox plots', function() { }, 1); }) .then(function() { - expect(gd.layout.mapbox.layers.length).toEqual(2); + expect(getLayerLength(gd)).toEqual(2); expect(countVisibleLayers(gd)).toEqual(2); // delete the first layer return Plotly.relayout(gd, 'mapbox.layers[0]', null); }) .then(function() { - expect(gd.layout.mapbox.layers.length).toEqual(1); + expect(getLayerLength(gd)).toEqual(1); expect(countVisibleLayers(gd)).toEqual(1); return Plotly.relayout(gd, 'mapbox.layers[0]', null); }) .then(function() { - expect(gd.layout.mapbox.layers).toBeUndefined(); + expect(getLayerLength(gd)).toEqual(0); expect(countVisibleLayers(gd)).toEqual(0); return Plotly.relayout(gd, 'mapbox.layers[0]', {}); @@ -637,7 +642,7 @@ describe('mapbox plots', function() { return Plotly.relayout(gd, 'mapbox.layers[0].source', layer0.source); }) .then(function() { - expect(gd.layout.mapbox.layers.length).toEqual(1); + expect(getLayerLength(gd)).toEqual(1); expect(countVisibleLayers(gd)).toEqual(1); }) .catch(failTest) diff --git a/test/jasmine/tests/plotschema_test.js b/test/jasmine/tests/plotschema_test.js index b7b227bc8a0..91cecf03bcd 100644 --- a/test/jasmine/tests/plotschema_test.js +++ b/test/jasmine/tests/plotschema_test.js @@ -204,7 +204,10 @@ describe('plot schema', function() { expect(plotSchema.defs.valObjects).toBeDefined(); expect(plotSchema.defs.metaKeys) - .toEqual(['_isSubplotObj', '_isLinkedToArray', '_deprecated', 'description', 'role']); + .toEqual([ + '_isSubplotObj', '_isLinkedToArray', '_arrayAttrRegexps', + '_deprecated', 'description', 'role' + ]); }); it('should list the correct frame attributes', function() { diff --git a/test/jasmine/tests/updatemenus_test.js b/test/jasmine/tests/updatemenus_test.js index f57635e0d3f..852f456d12f 100644 --- a/test/jasmine/tests/updatemenus_test.js +++ b/test/jasmine/tests/updatemenus_test.js @@ -315,6 +315,7 @@ describe('update menus interactions', function() { }); it('should draw only visible menus', function(done) { + var initialUM1 = Lib.extendDeep({}, gd.layout.updatemenus[1]); assertMenus([0, 0]); expect(gd._fullLayout._pushmargin['updatemenu-0']).toBeDefined(); expect(gd._fullLayout._pushmargin['updatemenu-1']).toBeDefined(); @@ -333,7 +334,7 @@ describe('update menus interactions', function() { return Plotly.relayout(gd, { 'updatemenus[0].visible': true, - 'updatemenus[1].visible': true + 'updatemenus[1]': initialUM1 }); }) .then(function() { @@ -655,7 +656,7 @@ describe('update menus interactions', function() { }); function assertNodeCount(query, cnt) { - expect(d3.selectAll(query).size()).toEqual(cnt); + expect(d3.selectAll(query).size()).toEqual(cnt, query); } // call assertMenus([0, 3]); to check that the 2nd update menu is dropped From 649a831cd9a3c3a08989e32db9dfbf813a8fc92d Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 21 Feb 2017 18:02:20 -0500 Subject: [PATCH 05/18] robustify click_test --- test/jasmine/tests/click_test.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/jasmine/tests/click_test.js b/test/jasmine/tests/click_test.js index 88a32102dcd..b38c7e1d476 100644 --- a/test/jasmine/tests/click_test.js +++ b/test/jasmine/tests/click_test.js @@ -14,7 +14,7 @@ var customMatchers = require('../assets/custom_matchers'); // from the mousemove events and then simulate // a click event on mouseup var click = require('../assets/click'); -var doubleClick = require('../assets/double_click'); +var doubleClickRaw = require('../assets/double_click'); describe('Test click interactions:', function() { @@ -52,6 +52,12 @@ describe('Test click interactions:', function() { }); } + function doubleClick(x, y) { + return doubleClickRaw(x, y).then(function() { + return Plotly.Plots.previousPromises(gd); + }); + } + describe('click events', function() { var futureData; From d55568c20e6306d988e4055232cc9d751384e69e Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 21 Feb 2017 18:08:41 -0500 Subject: [PATCH 06/18] :hocho: obsolete getShapeLayer --- src/components/shapes/draw.js | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index 90eee257e5d..fc019c7a1f8 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -271,22 +271,6 @@ function setupDragElement(gd, shapePath, shapeOptions, index) { } } -function getShapeLayer(gd, index) { - var shape = gd._fullLayout.shapes[index], - shapeLayer = gd._fullLayout._shapeUpperLayer; - - if(!shape) { - Lib.log('getShapeLayer: undefined shape: index', index); - } - else if(shape.layer === 'below') { - shapeLayer = (shape.xref === 'paper' && shape.yref === 'paper') ? - gd._fullLayout._shapeLowerLayer : - gd._fullLayout._shapeSubplotLayer; - } - - return shapeLayer; -} - function isShapeInSubplot(gd, shape, plotinfo) { var xa = Axes.getFromId(gd, plotinfo.id, 'x')._id, ya = Axes.getFromId(gd, plotinfo.id, 'y')._id, From 2e6c0301077eace4537968209638c1085a474d46 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 21 Feb 2017 19:02:33 -0500 Subject: [PATCH 07/18] break up new circular deps --- src/lib/identity.js | 14 +++++ src/lib/index.js | 38 ++------------ src/lib/nested_property.js | 2 +- src/lib/noop.js | 14 +++++ src/lib/push_unique.js | 36 +++++++++++++ src/plot_api/container_array_match.js | 56 ++++++++++++++++++++ src/plot_api/manage_arrays.js | 71 ++++++-------------------- src/registry.js | 22 ++++---- tasks/test_syntax.js | 5 +- test/jasmine/tests/annotations_test.js | 11 ++-- 10 files changed, 162 insertions(+), 107 deletions(-) create mode 100644 src/lib/identity.js create mode 100644 src/lib/noop.js create mode 100644 src/lib/push_unique.js create mode 100644 src/plot_api/container_array_match.js diff --git a/src/lib/identity.js b/src/lib/identity.js new file mode 100644 index 00000000000..426b69699a4 --- /dev/null +++ b/src/lib/identity.js @@ -0,0 +1,14 @@ +/** +* Copyright 2012-2017, 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'; + +// Simple helper functions +// none of these need any external deps + +module.exports = function identity(d) { return d; }; diff --git a/src/lib/index.js b/src/lib/index.js index 6291543317c..35c32508c24 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -81,10 +81,13 @@ lib.notifier = require('./notifier'); lib.filterUnique = require('./filter_unique'); lib.filterVisible = require('./filter_visible'); - +lib.pushUnique = require('./push_unique'); lib.cleanNumber = require('./clean_number'); +lib.noop = require('./noop'); +lib.identity = require('./identity'); + /** * swap x and y of the same attribute in container cont * specify attr with a ? in place of x/y @@ -136,12 +139,6 @@ lib.bBoxIntersect = function(a, b, pad) { b.top <= a.bottom + pad); }; -// minor convenience/performance booster for d3... -lib.identity = function(d) { return d; }; - -// minor convenience helper -lib.noop = function() {}; - /* * simpleMap: alternative to Array.map that only * passes on the element and up to 2 extra args you @@ -338,33 +335,6 @@ lib.noneOrAll = function(containerIn, containerOut, attrList) { } }; -/** - * Push array with unique items - * - * @param {array} array - * array to be filled - * @param {any} item - * item to be or not to be inserted - * @return {array} - * ref to array (now possibly containing one more item) - * - */ -lib.pushUnique = function(array, item) { - if(item instanceof RegExp) { - var itemStr = item.toString(), - i; - for(i = 0; i < array.length; i++) { - if(array[i] instanceof RegExp && array[i].toString() === itemStr) { - return array; - } - } - array.push(item); - } - else if(item && array.indexOf(item) === -1) array.push(item); - - return array; -}; - lib.mergeArray = function(traceAttr, cd, cdAttr) { if(Array.isArray(traceAttr)) { var imax = Math.min(traceAttr.length, cd.length); diff --git a/src/lib/nested_property.js b/src/lib/nested_property.js index 1feb4ba5599..a1201687bc9 100644 --- a/src/lib/nested_property.js +++ b/src/lib/nested_property.js @@ -12,7 +12,7 @@ var isNumeric = require('fast-isnumeric'); var isArray = require('./is_array'); var isPlainObject = require('./is_plain_object'); -var containerArrayMatch = require('../plot_api/manage_arrays').containerArrayMatch; +var containerArrayMatch = require('../plot_api/container_array_match'); /** * convert a string s (such as 'xaxis.range[0]') diff --git a/src/lib/noop.js b/src/lib/noop.js new file mode 100644 index 00000000000..e15fff56632 --- /dev/null +++ b/src/lib/noop.js @@ -0,0 +1,14 @@ +/** +* Copyright 2012-2017, 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'; + +// Simple helper functions +// none of these need any external deps + +module.exports = function noop() {}; diff --git a/src/lib/push_unique.js b/src/lib/push_unique.js new file mode 100644 index 00000000000..b0c8e54e91d --- /dev/null +++ b/src/lib/push_unique.js @@ -0,0 +1,36 @@ +/** +* Copyright 2012-2017, 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'; + +/** + * Push array with unique items + * + * @param {array} array + * array to be filled + * @param {any} item + * item to be or not to be inserted + * @return {array} + * ref to array (now possibly containing one more item) + * + */ +module.exports = function pushUnique(array, item) { + if(item instanceof RegExp) { + var itemStr = item.toString(), + i; + for(i = 0; i < array.length; i++) { + if(array[i] instanceof RegExp && array[i].toString() === itemStr) { + return array; + } + } + array.push(item); + } + else if(item && array.indexOf(item) === -1) array.push(item); + + return array; +}; diff --git a/src/plot_api/container_array_match.js b/src/plot_api/container_array_match.js new file mode 100644 index 00000000000..af844cd8ce8 --- /dev/null +++ b/src/plot_api/container_array_match.js @@ -0,0 +1,56 @@ +/** +* Copyright 2012-2017, 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 Registry = require('../registry'); + +/* + * containerArrayMatch: does this attribute string point into a + * layout container array? + * + * @param {String} astr: an attribute string, like *annotations[2].text* + * + * @returns {Object | false} Returns false if `astr` doesn't match a container + * array. If it does, returns: + * {array: {String}, index: {Number}, property: {String}} + * ie the attribute string for the array, the index within the array (or '' + * if the whole array) and the property within that (or '' if the whole array + * or the whole object) + */ +module.exports = function containerArrayMatch(astr) { + var rootContainers = Registry.layoutArrayContainers, + regexpContainers = Registry.layoutArrayRegexes, + rootPart = astr.split('[')[0], + arrayStr, + match; + + // look for regexp matches first, because they may be nested inside root matches + // eg updatemenus[i].buttons is nested inside updatemenus + for(var i = 0; i < regexpContainers.length; i++) { + match = astr.match(regexpContainers[i]); + if(match && match.index === 0) { + arrayStr = match[0]; + break; + } + } + + // now look for root matches + if(!arrayStr) arrayStr = rootContainers[rootContainers.indexOf(rootPart)]; + + if(!arrayStr) return false; + + var tail = astr.substr(arrayStr.length); + if(!tail) return {array: arrayStr, index: '', property: ''}; + + match = tail.match(/^\[(0|[1-9][0-9]*)\](\.(.+))?$/); + if(!match) return false; + + return {array: arrayStr, index: Number(match[1]), property: match[3] || ''}; +}; diff --git a/src/plot_api/manage_arrays.js b/src/plot_api/manage_arrays.js index 1bd76dbe940..4dca73cc801 100644 --- a/src/plot_api/manage_arrays.js +++ b/src/plot_api/manage_arrays.js @@ -9,62 +9,23 @@ 'use strict'; -var Lib = require('../lib'); +var nestedProperty = require('../lib/nested_property'); +var isPlainObject = require('../lib/is_plain_object'); +var noop = require('../lib/noop'); +var Loggers = require('../lib/loggers'); var Registry = require('../registry'); +exports.containerArrayMatch = require('./container_array_match'); + var isAddVal = exports.isAddVal = function isAddVal(val) { - return val === 'add' || Lib.isPlainObject(val); + return val === 'add' || isPlainObject(val); }; var isRemoveVal = exports.isRemoveVal = function isRemoveVal(val) { return val === null || val === 'remove'; }; -/* - * containerArrayMatch: does this attribute string point into a - * layout container array? - * - * @param {String} astr: an attribute string, like *annotations[2].text* - * - * @returns {Object | false} Returns false if `astr` doesn't match a container - * array. If it does, returns: - * {array: {String}, index: {Number}, property: {String}} - * ie the attribute string for the array, the index within the array (or '' - * if the whole array) and the property within that (or '' if the whole array - * or the whole object) - */ -exports.containerArrayMatch = function containerArrayMatch(astr) { - var rootContainers = Registry.layoutArrayContainers, - regexpContainers = Registry.layoutArrayRegexes, - rootPart = astr.split('[')[0], - arrayStr, - match; - - // look for regexp matches first, because they may be nested inside root matches - // eg updatemenus[i].buttons is nested inside updatemenus - for(var i = 0; i < regexpContainers.length; i++) { - match = astr.match(regexpContainers[i]); - if(match && match.index === 0) { - arrayStr = match[0]; - break; - } - } - - // now look for root matches - if(!arrayStr) arrayStr = rootContainers[rootContainers.indexOf(rootPart)]; - - if(!arrayStr) return false; - - var tail = astr.substr(arrayStr.length); - if(!tail) return {array: arrayStr, index: '', property: ''}; - - match = tail.match(/^\[(0|[1-9][0-9]*)\](\.(.+))?$/); - if(!match) return false; - - return {array: arrayStr, index: Number(match[1]), property: match[3] || ''}; -}; - /* * editContainerArray: for managing arrays of layout components in relayout * handles them all with a consistent interface. @@ -113,14 +74,14 @@ exports.editContainerArray = function editContainerArray(gd, np, edits, flags) { supplyComponentDefaults = Registry.getComponentMethod(componentType, 'supplyLayoutDefaults'), draw = Registry.getComponentMethod(componentType, 'draw'), drawOne = Registry.getComponentMethod(componentType, 'drawOne'), - replotLater = flags.replot || flags.recalc || (supplyComponentDefaults === Lib.noop) || - (draw === Lib.noop), + replotLater = flags.replot || flags.recalc || (supplyComponentDefaults === noop) || + (draw === noop), layout = gd.layout, fullLayout = gd._fullLayout; if(edits['']) { if(Object.keys(edits).length > 1) { - Lib.warn('Full array edits are incompatible with other edits', + Loggers.warn('Full array edits are incompatible with other edits', componentType); } @@ -129,7 +90,7 @@ exports.editContainerArray = function editContainerArray(gd, np, edits, flags) { if(isRemoveVal(fullVal)) np.set(null); else if(Array.isArray(fullVal)) np.set(fullVal); else { - Lib.warn('Unrecognized full array edit value', componentType, fullVal); + Loggers.warn('Unrecognized full array edit value', componentType, fullVal); return true; } @@ -164,13 +125,13 @@ exports.editContainerArray = function editContainerArray(gd, np, edits, flags) { adding = isAddVal(objVal); if(componentNum < 0 || componentNum > componentArray.length - (adding ? 0 : 1)) { - Lib.warn('index out of range', componentType, componentNum); + Loggers.warn('index out of range', componentType, componentNum); continue; } if(objVal !== undefined) { if(objKeys.length > 1) { - Lib.warn( + Loggers.warn( 'Insertion & removal are incompatible with edits to the same index.', componentType, componentNum); } @@ -183,7 +144,7 @@ exports.editContainerArray = function editContainerArray(gd, np, edits, flags) { componentArray.splice(componentNum, 0, objVal); } else { - Lib.warn('Unrecognized full object edit value', + Loggers.warn('Unrecognized full object edit value', componentType, componentNum, objVal); } @@ -191,7 +152,7 @@ exports.editContainerArray = function editContainerArray(gd, np, edits, flags) { } else { for(j = 0; j < objKeys.length; j++) { - Lib.nestedProperty(componentArray[componentNum], objKeys[j]).set(objEdits[objKeys[j]]); + nestedProperty(componentArray[componentNum], objKeys[j]).set(objEdits[objKeys[j]]); } } } @@ -210,7 +171,7 @@ exports.editContainerArray = function editContainerArray(gd, np, edits, flags) { // finally draw all the components we need to // if we added or removed any, redraw all after it - if(drawOne !== Lib.noop) { + if(drawOne !== noop) { var indicesToDraw; if(firstIndexChange === -1) { // there's no re-indexing to do, so only redraw components that changed diff --git a/src/registry.js b/src/registry.js index 64ff7c577f0..2952742f630 100644 --- a/src/registry.js +++ b/src/registry.js @@ -9,7 +9,9 @@ 'use strict'; -var Lib = require('./lib'); +var Loggers = require('./lib/loggers'); +var noop = require('./lib/noop'); +var pushUnique = require('./lib/push_unique'); var basePlotAttributes = require('./plots/attributes'); exports.modules = {}; @@ -32,7 +34,7 @@ exports.layoutArrayRegexes = []; */ exports.register = function(_module, thisType, categoriesIn, meta) { if(exports.modules[thisType]) { - Lib.log('Type ' + thisType + ' already registered'); + Loggers.log('Type ' + thisType + ' already registered'); return; } @@ -77,7 +79,7 @@ exports.registerSubplot = function(_module) { var plotType = _module.name; if(exports.subplotsRegistry[plotType]) { - Lib.log('Plot type ' + plotType + ' already registered.'); + Loggers.log('Plot type ' + plotType + ' already registered.'); return; } @@ -97,7 +99,7 @@ exports.registerComponent = function(_module) { if(_module.layoutAttributes) { if(_module.layoutAttributes._isLinkedToArray) { - Lib.pushUnique(exports.layoutArrayContainers, name); + pushUnique(exports.layoutArrayContainers, name); } findArrayRegexps(_module); } @@ -108,7 +110,7 @@ function findArrayRegexps(_module) { var arrayAttrRegexps = _module.layoutAttributes._arrayAttrRegexps; if(arrayAttrRegexps) { for(var i = 0; i < arrayAttrRegexps.length; i++) { - Lib.pushUnique(exports.layoutArrayRegexes, arrayAttrRegexps[i]); + pushUnique(exports.layoutArrayRegexes, arrayAttrRegexps[i]); } } } @@ -124,7 +126,7 @@ function findArrayRegexps(_module) { */ exports.getModule = function(trace) { if(trace.r !== undefined) { - Lib.warn('Tried to put a polar trace ' + + Loggers.warn('Tried to put a polar trace ' + 'on an incompatible graph of cartesian ' + 'data. Ignoring this dataset.', trace ); @@ -155,7 +157,7 @@ exports.traceIs = function(traceType, category) { if(!_module) { if(traceType && traceType !== 'area') { - Lib.log('Unrecognized trace type ' + traceType + '.'); + Loggers.log('Unrecognized trace type ' + traceType + '.'); } _module = exports.modules[basePlotAttributes.type.dflt]; @@ -165,7 +167,7 @@ exports.traceIs = function(traceType, category) { }; /** - * Retrieve component module method. Falls back on Lib.noop if either the + * Retrieve component module method. Falls back on noop if either the * module or the method is missing, so the result can always be safely called * * @param {string} name @@ -177,8 +179,8 @@ exports.traceIs = function(traceType, category) { exports.getComponentMethod = function(name, method) { var _module = exports.componentsRegistry[name]; - if(!_module) return Lib.noop; - return _module[method] || Lib.noop; + if(!_module) return noop; + return _module[method] || noop; }; function getTraceType(traceType) { diff --git a/tasks/test_syntax.js b/tasks/test_syntax.js index 66a31a33472..7aa6213e1d7 100644 --- a/tasks/test_syntax.js +++ b/tasks/test_syntax.js @@ -119,13 +119,14 @@ function assertCircularDeps() { // as of v1.17.0 - 2016/09/08 // see https://github.com/plotly/plotly.js/milestone/9 // for more details - var MAX_ALLOWED_CIRCULAR_DEPS = 18; + var MAX_ALLOWED_CIRCULAR_DEPS = 17; if(circularDeps.length > MAX_ALLOWED_CIRCULAR_DEPS) { + console.log(circularDeps.join('\n')); logs.push('some new circular dependencies were added to src/'); } - log('circular dependencies', logs); + log('circular dependencies: ' + circularDeps.length, logs); }); } diff --git a/test/jasmine/tests/annotations_test.js b/test/jasmine/tests/annotations_test.js index bc3d5fc8dd9..f537ba5ebe3 100644 --- a/test/jasmine/tests/annotations_test.js +++ b/test/jasmine/tests/annotations_test.js @@ -3,6 +3,7 @@ var Annotations = require('@src/components/annotations'); var Plotly = require('@lib/index'); var Plots = require('@src/plots/plots'); var Lib = require('@src/lib'); +var Loggers = require('@src/lib/loggers'); var Axes = require('@src/plots/cartesian/axes'); var d3 = require('d3'); @@ -146,7 +147,7 @@ describe('annotations relayout', function() { Plotly.plot(gd, mockData, mockLayout).then(done); - spyOn(Lib, 'warn'); + spyOn(Loggers, 'warn'); }); afterEach(destroyGraphDiv); @@ -200,7 +201,7 @@ describe('annotations relayout', function() { }) .then(function() { expect(countAnnotations()).toEqual(0); - expect(Lib.warn).not.toHaveBeenCalled(); + expect(Loggers.warn).not.toHaveBeenCalled(); }) .catch(failTest) .then(done); @@ -279,7 +280,7 @@ describe('annotations relayout', function() { assertText(3, 'lime'); anno3.text = 'lime'; expect(annos[3]).toEqual(anno3); - expect(Lib.warn).not.toHaveBeenCalled(); + expect(Loggers.warn).not.toHaveBeenCalled(); }) .catch(failTest) .then(done); @@ -298,7 +299,7 @@ describe('annotations relayout', function() { ].forEach(function(update) { it('warns on ambiguous combinations and invalid values: ' + JSON.stringify(update), function() { Plotly.relayout(gd, update); - expect(Lib.warn).toHaveBeenCalled(); + expect(Loggers.warn).toHaveBeenCalled(); // we could test the results here, but they're ambiguous and/or undefined so why bother? // the important thing is the developer is warned that something went wrong. }); @@ -342,7 +343,7 @@ describe('annotations relayout', function() { expect(annos[2].y).toBe(0.3); expect(annos[2].xref).toBe('paper'); expect(annos[2].yref).toBe('paper'); - expect(Lib.warn).not.toHaveBeenCalled(); + expect(Loggers.warn).not.toHaveBeenCalled(); }) .catch(failTest) .then(done); From f7e60fb78c9282e72edb485c2b8efd88e1bf35e0 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 21 Feb 2017 23:34:57 -0500 Subject: [PATCH 08/18] relinkPrivateKeys inside arrayContainerDefaults --- src/lib/index.js | 1 + src/lib/relink_private.js | 55 +++++++++++++++++++++++++++ src/plot_api/plot_api.js | 3 -- src/plots/array_container_defaults.js | 18 +++++++-- src/plots/plots.js | 47 +---------------------- 5 files changed, 73 insertions(+), 51 deletions(-) create mode 100644 src/lib/relink_private.js diff --git a/src/lib/index.js b/src/lib/index.js index 35c32508c24..e1b6475a29b 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -18,6 +18,7 @@ lib.isPlainObject = require('./is_plain_object'); lib.isArray = require('./is_array'); lib.mod = require('./mod'); lib.toLogRange = require('./to_log_range'); +lib.relinkPrivateKeys = require('./relink_private'); var coerceModule = require('./coerce'); lib.valObjects = coerceModule.valObjects; diff --git a/src/lib/relink_private.js b/src/lib/relink_private.js new file mode 100644 index 00000000000..223ac3c5fc6 --- /dev/null +++ b/src/lib/relink_private.js @@ -0,0 +1,55 @@ +/** +* Copyright 2012-2017, 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 isArray = require('./is_array'); +var isPlainObject = require('./is_plain_object'); + +/** + * Relink private _keys and keys with a function value from one container + * to the new container. + * Relink means copying if object is pass-by-value and adding a reference + * if object is pass-by-ref. + * This prevents deepCopying massive structures like a webgl context. + */ +module.exports = function relinkPrivateKeys(toContainer, fromContainer) { + var keys = Object.keys(fromContainer || {}); + + for(var i = 0; i < keys.length; i++) { + var k = keys[i], + fromVal = fromContainer[k], + toVal = toContainer[k]; + + if(k.charAt(0) === '_' || typeof fromVal === 'function') { + + // if it already exists at this point, it's something + // that we recreate each time around, so ignore it + if(k in toContainer) continue; + + toContainer[k] = fromVal; + } + else if(isArray(fromVal) && isArray(toVal) && isPlainObject(fromVal[0])) { + + // recurse into arrays containers + for(var j = 0; j < fromVal.length; j++) { + if(isPlainObject(fromVal[j]) && isPlainObject(toVal[j])) { + relinkPrivateKeys(toVal[j], fromVal[j]); + } + } + } + else if(isPlainObject(fromVal) && isPlainObject(toVal)) { + + // recurse into objects, but only if they still exist + relinkPrivateKeys(toVal, fromVal); + + if(!Object.keys(toVal).length) delete toContainer[k]; + } + } +}; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 428d80f4206..3a3066b4908 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -2092,9 +2092,6 @@ function _relayout(gd, aobj) { var oldWidth = gd._fullLayout.width, oldHeight = gd._fullLayout.height; - // coerce the updated layout - Plots.supplyDefaults(gd); - // calculate autosizing if(gd.layout.autosize) Plots.plotAutoSize(gd, gd.layout, gd._fullLayout); diff --git a/src/plots/array_container_defaults.js b/src/plots/array_container_defaults.js index 2754ed613c2..7a0093fa3f1 100644 --- a/src/plots/array_container_defaults.js +++ b/src/plots/array_container_defaults.js @@ -44,10 +44,13 @@ var Lib = require('../lib'); module.exports = function handleArrayContainerDefaults(parentObjIn, parentObjOut, opts) { var name = opts.name; - var contIn = Array.isArray(parentObjIn[name]) ? parentObjIn[name] : [], - contOut = parentObjOut[name] = []; + var previousContOut = parentObjOut[name]; - for(var i = 0; i < contIn.length; i++) { + var contIn = Lib.isArray(parentObjIn[name]) ? parentObjIn[name] : [], + contOut = parentObjOut[name] = [], + i; + + for(i = 0; i < contIn.length; i++) { var itemIn = contIn[i], itemOut = {}, itemOpts = {}; @@ -64,4 +67,13 @@ module.exports = function handleArrayContainerDefaults(parentObjIn, parentObjOut contOut.push(itemOut); } + + // in case this array gets its defaults rebuilt independent of the whole layout, + // relink the private keys just for this array. + if(Lib.isArray(previousContOut)) { + var len = Math.min(previousContOut.length, contOut.length); + for(i = 0; i < len; i++) { + Lib.relinkPrivateKeys(contOut[i], previousContOut[i]); + } + } }; diff --git a/src/plots/plots.js b/src/plots/plots.js index 3b2e3301586..b18837d7ef3 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -22,6 +22,8 @@ var plots = module.exports = {}; var animationAttrs = require('./animation_attributes'); var frameAttrs = require('./frame_attributes'); +var relinkPrivateKeys = Lib.relinkPrivateKeys; + // Expose registry methods on Plots for backward-compatibility Lib.extendFlat(plots, Registry); @@ -592,51 +594,6 @@ plots.cleanPlot = function(newFullData, newFullLayout, oldFullData, oldFullLayou } }; -/** - * Relink private _keys and keys with a function value from one container - * to the new container. - * Relink means copying if object is pass-by-value and adding a reference - * if object is pass-by-ref. - * This prevents deepCopying massive structures like a webgl context. - */ -function relinkPrivateKeys(toContainer, fromContainer) { - var isPlainObject = Lib.isPlainObject, - isArray = Array.isArray; - - var keys = Object.keys(fromContainer || {}); - - for(var i = 0; i < keys.length; i++) { - var k = keys[i], - fromVal = fromContainer[k], - toVal = toContainer[k]; - - if(k.charAt(0) === '_' || typeof fromVal === 'function') { - - // if it already exists at this point, it's something - // that we recreate each time around, so ignore it - if(k in toContainer) continue; - - toContainer[k] = fromVal; - } - else if(isArray(fromVal) && isArray(toVal) && isPlainObject(fromVal[0])) { - - // recurse into arrays containers - for(var j = 0; j < fromVal.length; j++) { - if(isPlainObject(fromVal[j]) && isPlainObject(toVal[j])) { - relinkPrivateKeys(toVal[j], fromVal[j]); - } - } - } - else if(isPlainObject(fromVal) && isPlainObject(toVal)) { - - // recurse into objects, but only if they still exist - relinkPrivateKeys(toVal, fromVal); - - if(!Object.keys(toVal).length) delete toContainer[k]; - } - } -} - plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { var oldSubplots = oldFullLayout._plots || {}, newSubplots = newFullLayout._plots = {}; From d7cdc0a1a6e83fb19595890cd98bbfdce41b3005 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 21 Feb 2017 23:58:01 -0500 Subject: [PATCH 09/18] revert layout_image.json - keep #1390 separate for now --- test/image/mocks/layout_image.json | 1 - 1 file changed, 1 deletion(-) diff --git a/test/image/mocks/layout_image.json b/test/image/mocks/layout_image.json index 06ef34f1d64..200fb0f9c51 100644 --- a/test/image/mocks/layout_image.json +++ b/test/image/mocks/layout_image.json @@ -11,7 +11,6 @@ } ], "layout": { - "plot_bgcolor": "rgba(0,0,0,0)", "xaxis2": { "anchor": "y2" }, From 7ed9eb9ffe052c4b2cba183c753e130a42074cb3 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 22 Feb 2017 01:18:06 -0500 Subject: [PATCH 10/18] fix for components using category names as coordinates --- src/components/images/defaults.js | 7 ++++--- src/plot_api/plot_api.js | 2 +- src/plots/plots.js | 15 +++++++++++++++ test/image/baselines/layout_image.png | Bin 67152 -> 66517 bytes test/image/baselines/shapes.png | Bin 27190 -> 28914 bytes test/image/mocks/layout_image.json | 4 ++-- test/image/mocks/shapes.json | 1 + 7 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/components/images/defaults.js b/src/components/images/defaults.js index 0c6c5b32c93..8073db69aa9 100644 --- a/src/components/images/defaults.js +++ b/src/components/images/defaults.js @@ -37,8 +37,6 @@ function imageDefaults(imageIn, imageOut, fullLayout) { if(!visible) return imageOut; coerce('layer'); - coerce('x'); - coerce('y'); coerce('xanchor'); coerce('yanchor'); coerce('sizex'); @@ -51,7 +49,10 @@ function imageDefaults(imageIn, imageOut, fullLayout) { for(var i = 0; i < 2; i++) { // 'paper' is the fallback axref - Axes.coerceRef(imageIn, imageOut, gdMock, axLetters[i], 'paper'); + var axLetter = axLetters[i], + axRef = Axes.coerceRef(imageIn, imageOut, gdMock, axLetter, 'paper'); + + Axes.coercePosition(imageOut, gdMock, coerce, axRef, axLetter, 0); } return imageOut; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 3a3066b4908..935709e8445 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -251,7 +251,7 @@ Plotly.plot = function(gd, data, layout, config) { // calc and autorange for errorbars ErrorBars.calc(gd); - // TODO: autosize extra for text markers + // TODO: autosize extra for text markers and images return Lib.syncOrAsync([ Registry.getComponentMethod('shapes', 'calcAutorange'), Registry.getComponentMethod('annotations', 'calcAutorange'), diff --git a/src/plots/plots.js b/src/plots/plots.js index b18837d7ef3..d241eb09a78 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -1911,6 +1911,8 @@ plots.doCalcdata = function(gd, traces) { var trace, _module, i, j; + var hasCategoryAxis = false; + // XXX: Is this correct? Needs a closer look so that *some* traces can be recomputed without // *all* needing doCalcdata: var calcdata = new Array(fullData.length); @@ -1936,6 +1938,7 @@ plots.doCalcdata = function(gd, traces) { // to be filled in later by ax.d2c for(i = 0; i < axList.length; i++) { axList[i]._categories = axList[i]._initialCategories.slice(); + if(axList[i].type === 'category') hasCategoryAxis = true; } // If traces were specified and this trace was not included, @@ -2012,6 +2015,18 @@ plots.doCalcdata = function(gd, traces) { calcdata[i] = cd; } + + // To handle the case of components using category names as coordinates, we + // need to re-supply defaults for these objects now, after calc has + // finished populating the category mappings + // Any component that uses `Axes.coercePosition` falls into this category + if(hasCategoryAxis) { + var dataReferencedComponents = ['annotations', 'shapes', 'images']; + for(i = 0; i < dataReferencedComponents.length; i++) { + Registry.getComponentMethod(dataReferencedComponents[i], 'supplyLayoutDefaults')( + gd.layout, fullLayout, fullData); + } + } }; plots.rehover = function(gd) { diff --git a/test/image/baselines/layout_image.png b/test/image/baselines/layout_image.png index def47ebab0fcaeadcaeee22ca3d9ec05a1a15506..1ea7021287a725cdaf4991e083ee3745528d4868 100644 GIT binary patch delta 38336 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 Date: Wed, 22 Feb 2017 15:54:46 -0500 Subject: [PATCH 11/18] fix for autorange with bad axis refs from a component --- src/plot_api/plot_api.js | 18 +++++-- test/jasmine/tests/annotations_test.js | 74 +++++++++++++++++--------- 2 files changed, 64 insertions(+), 28 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 935709e8445..67c7e99d43c 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -1822,10 +1822,22 @@ function _relayout(gd, aobj) { } // for editing annotations or shapes - is it on autoscaled axes? - function refAutorange(obj, axletter) { + function refAutorange(obj, axLetter) { if(!Lib.isPlainObject(obj)) return false; - var axName = Plotly.Axes.id2name(obj[axletter + 'ref'] || axletter); - return (fullLayout[axName] || {}).autorange; + var axRef = obj[axLetter + 'ref'] || axLetter, + ax = Plotly.Axes.getFromId(gd, axRef); + + if(!ax && axRef.charAt(0) === axLetter) { + // fall back on the primary axis in case we've referenced a + // nonexistent axis (as we do above if axRef is missing). + // This assumes the object defaults to data referenced, which + // is the case for shapes and annotations but not for images. + // The only thing this is used for is to determine whether to + // do a full `recalc`, so the only ill effect of this error is + // to waste some time. + ax = Plotly.Axes.getFromId(gd, axLetter); + } + return (ax || {}).autorange; } // alter gd.layout diff --git a/test/jasmine/tests/annotations_test.js b/test/jasmine/tests/annotations_test.js index f537ba5ebe3..e5899764926 100644 --- a/test/jasmine/tests/annotations_test.js +++ b/test/jasmine/tests/annotations_test.js @@ -474,7 +474,7 @@ describe('annotations log/linear axis changes', function() { }); -describe('annotations autosize', function() { +describe('annotations autorange', function() { 'use strict'; var mock = Lib.extendDeep({}, require('@mocks/annotations-autorange.json')); @@ -482,35 +482,35 @@ describe('annotations autosize', function() { beforeAll(function() { jasmine.addMatchers(customMatchers); + + gd = createGraphDiv(); }); afterEach(destroyGraphDiv); - it('should adapt to relayout calls', function(done) { - gd = createGraphDiv(); - - function assertRanges(x, y, x2, y2, x3, y3) { - var fullLayout = gd._fullLayout; - var PREC = 1; - - // xaxis2 need a bit more tolerance to pass on CI - // this most likely due to the different text bounding box values - // on headfull vs headless browsers. - // but also because it's a date axis that we've converted to ms - var PRECX2 = -10; - // yaxis2 needs a bit more now too... - var PRECY2 = 0.2; - var dateAx = fullLayout.xaxis2; - - expect(fullLayout.xaxis.range).toBeCloseToArray(x, PREC, '- xaxis'); - expect(fullLayout.yaxis.range).toBeCloseToArray(y, PREC, '- yaxis'); - expect(Lib.simpleMap(dateAx.range, dateAx.r2l)) - .toBeCloseToArray(Lib.simpleMap(x2, dateAx.r2l), PRECX2, 'xaxis2 ' + dateAx.range); - expect(fullLayout.yaxis2.range).toBeCloseToArray(y2, PRECY2, 'yaxis2'); - expect(fullLayout.xaxis3.range).toBeCloseToArray(x3, PREC, 'xaxis3'); - expect(fullLayout.yaxis3.range).toBeCloseToArray(y3, PREC, 'yaxis3'); - } + function assertRanges(x, y, x2, y2, x3, y3) { + var fullLayout = gd._fullLayout; + var PREC = 1; + + // xaxis2 need a bit more tolerance to pass on CI + // this most likely due to the different text bounding box values + // on headfull vs headless browsers. + // but also because it's a date axis that we've converted to ms + var PRECX2 = -10; + // yaxis2 needs a bit more now too... + var PRECY2 = 0.2; + var dateAx = fullLayout.xaxis2; + + expect(fullLayout.xaxis.range).toBeCloseToArray(x, PREC, '- xaxis'); + expect(fullLayout.yaxis.range).toBeCloseToArray(y, PREC, '- yaxis'); + expect(Lib.simpleMap(dateAx.range, dateAx.r2l)) + .toBeCloseToArray(Lib.simpleMap(x2, dateAx.r2l), PRECX2, 'xaxis2 ' + dateAx.range); + expect(fullLayout.yaxis2.range).toBeCloseToArray(y2, PRECY2, 'yaxis2'); + expect(fullLayout.xaxis3.range).toBeCloseToArray(x3, PREC, 'xaxis3'); + expect(fullLayout.yaxis3.range).toBeCloseToArray(y3, PREC, 'yaxis3'); + } + it('should adapt to relayout calls', function(done) { Plotly.plot(gd, mock).then(function() { assertRanges( [0.97, 2.03], [0.97, 2.03], @@ -563,6 +563,30 @@ describe('annotations autosize', function() { .catch(failTest) .then(done); }); + + it('catches bad xref/yref', function(done) { + Plotly.plot(gd, mock).then(function() { + return Plotly.relayout(gd, {'annotations[1]': { + text: 'LT', + x: -1, + y: 3, + xref: 'x5', // will be converted to 'x' and xaxis should autorange + yref: 'y5', // same 'y' -> yaxis + ax: 50, + ay: 50 + }}); + }) + .then(function() { + assertRanges( + [-1.09, 2.09], [0.94, 3.06], + // the other axes shouldn't change + ['2000-10-01 08:23:18.0583', '2001-06-05 19:20:23.301'], [-0.245, 4.245], + [0.9, 2.1], [0.86, 2.14] + ); + }) + .catch(failTest) + .then(done); + }); }); describe('annotation clicktoshow', function() { From 46962af8bdb37ee77bef8880c41baf7186f095ac Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 22 Feb 2017 15:58:26 -0500 Subject: [PATCH 12/18] point to open issue #1111 --- src/plot_api/plot_api.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 67c7e99d43c..36f449b43a7 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -252,6 +252,7 @@ Plotly.plot = function(gd, data, layout, config) { ErrorBars.calc(gd); // TODO: autosize extra for text markers and images + // see https://github.com/plotly/plotly.js/issues/1111 return Lib.syncOrAsync([ Registry.getComponentMethod('shapes', 'calcAutorange'), Registry.getComponentMethod('annotations', 'calcAutorange'), From 1af058ab343062365eadb5ec0c60251c4c78eabf Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 23 Feb 2017 00:14:22 -0500 Subject: [PATCH 13/18] allow all `info_array`s to be pruned and command args to be optional --- src/lib/nested_property.js | 25 +++++++++---------------- src/plots/command.js | 8 ++++++++ test/jasmine/tests/sliders_test.js | 6 +++--- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/lib/nested_property.js b/src/lib/nested_property.js index a1201687bc9..15920496cd5 100644 --- a/src/lib/nested_property.js +++ b/src/lib/nested_property.js @@ -114,30 +114,23 @@ function npGet(cont, parts) { }; } -/* - * Check known non-data-array arrays (containers). Data arrays only contain scalars, - * so parts[end] values, such as -1 or n, indicate we are not dealing with a dataArray. - * The ONLY case we are looking for is where the entire array is selected, parts[end] === 'x' - * AND the replacement value is an array. - */ -// function isNotAContainer(key) { -// var containers = ['annotations', 'shapes', 'range', 'domain', 'buttons']; - -// return containers.indexOf(key) === -1; -// } - /* * Can this value be deleted? We can delete any empty object (null, undefined, [], {}) - * EXCEPT empty data arrays. If it's not a data array, it's a container array, - * ie containing objects like annotations, buttons, etc + * EXCEPT empty data arrays. Info arrays can be safely deleted, but not deleting them + * has no ill effects other than leaving a trace or layout object with some cruft in it. + * But deleting data arrays can change the meaning of the object, as `[]` means there is + * data for this attribute, it's just empty right now while `undefined` means the data + * should be filled in with defaults to match other data arrays. So we do some simple + * tests here to find known non-data arrays but don't worry too much about not deleting + * some arrays that would actually be safe to delete. */ -var DOMAIN_RANGE = /(^|.)(domain|range)$/; +var INFO_PATTERNS = /(^|.)((domain|range)(\.[xy])?|args|parallels)$/; function isDeletable(val, propStr) { if(!emptyObj(val)) return false; if(!isArray(val)) return true; // domain and range are special - they show up in lots of places so hard code here. - if(propStr.match(DOMAIN_RANGE)) return true; + if(propStr.match(INFO_PATTERNS)) return true; var match = containerArrayMatch(propStr); // if propStr matches the container array itself, index is an empty string diff --git a/src/plots/command.js b/src/plots/command.js index 62c060edd5e..830af6db804 100644 --- a/src/plots/command.js +++ b/src/plots/command.js @@ -156,6 +156,8 @@ exports.hasSimpleAPICommandBindings = function(gd, commandList, bindingsByValue) var method = command.method; var args = command.args; + if(!Array.isArray(args)) args = []; + // If any command has no method, refuse to bind: if(!method) { return false; @@ -263,6 +265,9 @@ exports.executeAPICommand = function(gd, method, args) { var apiMethod = Plotly[method]; var allArgs = [gd]; + + if(!Array.isArray(args)) args = []; + for(var i = 0; i < args.length; i++) { allArgs.push(args[i]); } @@ -275,6 +280,9 @@ exports.executeAPICommand = function(gd, method, args) { exports.computeAPICommandBindings = function(gd, method, args) { var bindings; + + if(!Array.isArray(args)) args = []; + switch(method) { case 'restyle': bindings = computeDataBindings(gd, args); diff --git a/test/jasmine/tests/sliders_test.js b/test/jasmine/tests/sliders_test.js index 5a1274769a2..377c82d70aa 100644 --- a/test/jasmine/tests/sliders_test.js +++ b/test/jasmine/tests/sliders_test.js @@ -97,15 +97,15 @@ describe('sliders defaults', function() { expect(layoutOut.sliders[0].steps.length).toEqual(3); expect(layoutOut.sliders[0].steps).toEqual([{ - method: 'relayout', args: [], + method: 'relayout', label: 'Label #1', value: 'label-1' }, { - method: 'update', args: [], + method: 'update', label: 'Label #2', value: 'Label #2' }, { - method: 'animate', args: [], + method: 'animate', label: 'step-2', value: 'lacks-label' }]); From c6378b12e0d2cd99e7fc2541a6dfcf3b4597fdc2 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 23 Feb 2017 13:49:24 -0500 Subject: [PATCH 14/18] exclude `args` from `nestedProperty` pruning --- src/lib/nested_property.js | 47 ++++++++++++++++++++++------------ test/jasmine/tests/lib_test.js | 24 +++++++++++++++++ 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/src/lib/nested_property.js b/src/lib/nested_property.js index 15920496cd5..42db6baf574 100644 --- a/src/lib/nested_property.js +++ b/src/lib/nested_property.js @@ -116,20 +116,36 @@ function npGet(cont, parts) { /* * Can this value be deleted? We can delete any empty object (null, undefined, [], {}) - * EXCEPT empty data arrays. Info arrays can be safely deleted, but not deleting them - * has no ill effects other than leaving a trace or layout object with some cruft in it. - * But deleting data arrays can change the meaning of the object, as `[]` means there is + * EXCEPT empty data arrays, {} inside an array, or anything INSIDE an *args* array. + * + * Info arrays can be safely deleted, but not deleting them has no ill effects other + * than leaving a trace or layout object with some cruft in it. + * + * Deleting data arrays can change the meaning of the object, as `[]` means there is * data for this attribute, it's just empty right now while `undefined` means the data - * should be filled in with defaults to match other data arrays. So we do some simple - * tests here to find known non-data arrays but don't worry too much about not deleting - * some arrays that would actually be safe to delete. + * should be filled in with defaults to match other data arrays. + * + * `{}` inside an array means "the default object" which is clearly different from + * popping it off the end of the array, or setting it `undefined` inside the array. + * + * *args* arrays get passed directly to API methods and we should respect precisely + * what the user has put there - although if the whole *args* array is empty it's fine + * to delete that. + * + * So we do some simple tests here to find known non-data arrays but don't worry too + * much about not deleting some arrays that would actually be safe to delete. */ -var INFO_PATTERNS = /(^|.)((domain|range)(\.[xy])?|args|parallels)$/; +var INFO_PATTERNS = /(^|\.)((domain|range)(\.[xy])?|args|parallels)$/; +var ARGS_PATTERN = /(^|\.)args\[/; function isDeletable(val, propStr) { - if(!emptyObj(val)) return false; + if(!emptyObj(val) || + (isPlainObject(val) && propStr.charAt(propStr.length - 1) === ']') || + (propStr.match(ARGS_PATTERN) && val !== undefined) + ) { + return false; + } if(!isArray(val)) return true; - // domain and range are special - they show up in lots of places so hard code here. if(propStr.match(INFO_PATTERNS)) return true; var match = containerArrayMatch(propStr); @@ -186,8 +202,11 @@ function npSet(cont, parts, propStr) { } function joinPropStr(propStr, newPart) { - if(!propStr) return newPart; - return propStr + isNumeric(newPart) ? ('[' + newPart + ']') : ('.' + newPart); + var toAdd = newPart; + if(isNumeric(newPart)) toAdd = '[' + newPart + ']'; + else if(propStr) toAdd = '.' + newPart; + + return propStr + toAdd; } // handle special -1 array index @@ -244,11 +263,7 @@ function pruneContainers(containerLevels) { remainingKeys = false; if(isArray(curCont)) { for(j = curCont.length - 1; j >= 0; j--) { - // If there's a plain object in an array, it's a container array - // so we don't delete empty containers because they still have meaning. - // `editContainerArray` handles the API for adding/removing objects - // in this case. - if(emptyObj(curCont[j]) && !isPlainObject(curCont[j])) { + if(isDeletable(curCont[j], joinPropStr(propPart, j))) { if(remainingKeys) curCont[j] = undefined; else curCont.pop(); } diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index 189a3835cf9..edd5abfdf71 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -375,6 +375,30 @@ describe('Test lib.js:', function() { expect(obj).toEqual({a: [undefined, {}]}); }); + it('does not prune inside `args` arrays', function() { + var obj = {}, + args = np(obj, 'args'); + + args.set([]); + expect(obj.args).toBeUndefined(); + + args.set([null]); + expect(obj.args).toEqual([null]); + + np(obj, 'args[1]').set([]); + expect(obj.args).toEqual([null, []]); + + np(obj, 'args[2]').set({}); + expect(obj.args).toEqual([null, [], {}]); + + np(obj, 'args[1]').set(); + expect(obj.args).toEqual([null, undefined, {}]); + + // we still trim undefined off the end of arrays, but nothing else. + np(obj, 'args[2]').set(); + expect(obj.args).toEqual([null]); + }); + it('should get empty, and fail on set, with a bad input object', function() { var badProps = [ np(5, 'a'), From 70c882bceaa5236b7c1e7cdf51903c5d1823449b Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 23 Feb 2017 14:26:07 -0500 Subject: [PATCH 15/18] keep _fullLayout container arrays in sync with splices in layout --- src/plot_api/manage_arrays.js | 12 +++++++++++- test/jasmine/tests/annotations_test.js | 23 +++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/plot_api/manage_arrays.js b/src/plot_api/manage_arrays.js index 4dca73cc801..1ac89f62d72 100644 --- a/src/plot_api/manage_arrays.js +++ b/src/plot_api/manage_arrays.js @@ -103,7 +103,13 @@ exports.editContainerArray = function editContainerArray(gd, np, edits, flags) { var componentNums = Object.keys(edits).map(Number).sort(), componentArrayIn = np.get(), - componentArray = componentArrayIn || []; + componentArray = componentArrayIn || [], + // componentArrayFull is used just to keep splices in line between + // full and input arrays, so private keys can be copied over after + // redoing supplyDefaults + // TODO: this assumes componentArray is in gd.layout - which will not be + // true after we extend this to restyle + componentArrayFull = nestedProperty(fullLayout, componentType).get(); var deletes = [], firstIndexChange = -1, @@ -142,6 +148,7 @@ exports.editContainerArray = function editContainerArray(gd, np, edits, flags) { else if(adding) { if(objVal === 'add') objVal = {}; componentArray.splice(componentNum, 0, objVal); + if(componentArrayFull) componentArrayFull.splice(componentNum, 0, {}); } else { Loggers.warn('Unrecognized full object edit value', @@ -160,6 +167,9 @@ exports.editContainerArray = function editContainerArray(gd, np, edits, flags) { // now do deletes for(i = deletes.length - 1; i >= 0; i--) { componentArray.splice(deletes[i], 1); + // TODO: this drops private keys that had been stored in componentArrayFull + // does this have any ill effects? + if(componentArrayFull) componentArrayFull.splice(deletes[i], 1); } if(!componentArray.length) np.set(null); diff --git a/test/jasmine/tests/annotations_test.js b/test/jasmine/tests/annotations_test.js index e5899764926..d898a65341a 100644 --- a/test/jasmine/tests/annotations_test.js +++ b/test/jasmine/tests/annotations_test.js @@ -255,6 +255,15 @@ describe('annotations relayout', function() { anno1 = Lib.extendFlat(annos[1]), anno3 = Lib.extendFlat(annos[3]); + // store some (unused) private keys and make sure they are copied over + // correctly during relayout + var fullAnnos = gd._fullLayout.annotations; + fullAnnos[0]._boo = 'hoo'; + fullAnnos[1]._foo = 'bar'; + fullAnnos[3]._cheese = ['gorgonzola', 'gouda', 'gloucester']; + // this one gets lost + fullAnnos[2]._splat = 'the cat'; + Plotly.relayout(gd, { 'annotations[0].text': 'tortilla', 'annotations[0].x': 3.45, @@ -266,21 +275,35 @@ describe('annotations relayout', function() { .then(function() { expect(countAnnotations()).toEqual(len); + var fullAnnosAfter = gd._fullLayout.annotations, + fullStr = JSON.stringify(fullAnnosAfter); + assertText(0, 'tortilla'); anno0.text = 'tortilla'; expect(annos[0]).toEqual(anno0); + expect(fullAnnosAfter[0]._boo).toBe('hoo'); + assertText(1, 'chips'); expect(annos[1]).toEqual({text: 'chips', x: 1.1, y: 2.2}); + expect(fullAnnosAfter[1]._foo).toBeUndefined(); assertText(2, 'guacamole'); anno1.text = 'guacamole'; expect(annos[2]).toEqual(anno1); + expect(fullAnnosAfter[2]._foo).toBe('bar'); + expect(fullAnnosAfter[2]._splat).toBeUndefined(); assertText(3, 'lime'); anno3.text = 'lime'; expect(annos[3]).toEqual(anno3); + expect(fullAnnosAfter[3]._cheese).toEqual(['gorgonzola', 'gouda', 'gloucester']); + + expect(fullStr.indexOf('_splat')).toBe(-1); + expect(fullStr.indexOf('the cat')).toBe(-1); + expect(Loggers.warn).not.toHaveBeenCalled(); + }) .catch(failTest) .then(done); From 5fb462fa6b8c256e5fc179c028d59c10ce5c7b6d Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 24 Feb 2017 11:23:56 -0500 Subject: [PATCH 16/18] comments and tests for plot_api/helpers.hasParent --- src/plot_api/helpers.js | 17 ++++++++++ test/jasmine/tests/plot_api_test.js | 50 +++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/src/plot_api/helpers.js b/src/plot_api/helpers.js index 64a265e354c..26c03943d6a 100644 --- a/src/plot_api/helpers.js +++ b/src/plot_api/helpers.js @@ -485,6 +485,12 @@ exports.manageArrayContainers = function(np, newVal, undoit) { } }; +/* + * Match the part to strip off to turn an attribute into its parent + * really it should be either '.some_characters' or '[number]' + * but we're a little more permissive here and match either + * '.not_brackets_or_dot' or '[not_brackets_or_dot]' + */ var ATTR_TAIL_RE = /(\.[^\[\]\.]+|\[[^\[\]\.]+\])$/; function getParent(attr) { @@ -492,6 +498,17 @@ function getParent(attr) { if(tail > 0) return attr.substr(0, tail); } +/* + * hasParent: does an attribute object contain a parent of the given attribute? + * for example, given 'images[2].x' do we also have 'images' or 'images[2]'? + * + * @param {Object} aobj + * update object, whose keys are attribute strings and values are their new settings + * @param {string} attr + * the attribute string to test against + * @returns {Boolean} + * is a parent of attr present in aobj? + */ exports.hasParent = function(aobj, attr) { var attrParent = getParent(attr); while(attrParent) { diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index e6deca855b0..19419f7cb06 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -7,6 +7,7 @@ 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 helpers = require('@src/plot_api/helpers'); var d3 = require('d3'); var createGraphDiv = require('../assets/create_graph_div'); @@ -1309,3 +1310,52 @@ describe('Test plot api', function() { }); }); }); + +describe('plot_api helpers', function() { + describe('hasParent', function() { + var attr = 'annotations[2].xref'; + var attr2 = 'marker.line.width'; + + it('does not match the attribute itself or other related non-parent attributes', function() { + var aobj = { + // '' wouldn't be valid as an attribute in our framework, but tested + // just in case this would count as a parent. + '': true, + 'annotations[1]': {}, // parent structure, just a different array element + 'xref': 1, // another substring + 'annotations[2].x': 0.5, // substring of the attribute, but not a parent + 'annotations[2].xref': 'x2' // the attribute we're testing - not its own parent + }; + + expect(helpers.hasParent(aobj, attr)).toBe(false); + + var aobj2 = { + 'marker.line.color': 'red', + 'marker.line.width': 2, + 'marker.color': 'blue', + 'line': {} + }; + + expect(helpers.hasParent(aobj2, attr2)).toBe(false); + }); + + it('is false when called on a top-level attribute', function() { + var aobj = { + '': true, + 'width': 100 + }; + + expect(helpers.hasParent(aobj, 'width')).toBe(false); + }); + + it('matches any kind of parent', function() { + expect(helpers.hasParent({'annotations': []}, attr)).toBe(true); + expect(helpers.hasParent({'annotations[2]': {}}, attr)).toBe(true); + + expect(helpers.hasParent({'marker': {}}, attr2)).toBe(true); + // this one wouldn't actually make sense: marker.line needs to be an object... + // but hasParent doesn't look at the values in aobj, just its keys. + expect(helpers.hasParent({'marker.line': 1}, attr2)).toBe(true); + }); + }); +}); From d29cbae52654f1e74d29e77dbc353d021353eb74 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 24 Feb 2017 12:01:47 -0500 Subject: [PATCH 17/18] editContainerArray => applyContainerArrayChanges --- src/plot_api/manage_arrays.js | 4 ++-- src/plot_api/plot_api.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plot_api/manage_arrays.js b/src/plot_api/manage_arrays.js index 1ac89f62d72..d408496c208 100644 --- a/src/plot_api/manage_arrays.js +++ b/src/plot_api/manage_arrays.js @@ -27,7 +27,7 @@ var isRemoveVal = exports.isRemoveVal = function isRemoveVal(val) { }; /* - * editContainerArray: for managing arrays of layout components in relayout + * applyContainerArrayChanges: for managing arrays of layout components in relayout * handles them all with a consistent interface. * * Here are the supported actions -> relayout calls -> edits we get here @@ -69,7 +69,7 @@ var isRemoveVal = exports.isRemoveVal = function isRemoveVal(val) { * @returns {bool} `true` if it managed to complete drawing of the changes * `false` would mean the parent should replot. */ -exports.editContainerArray = function editContainerArray(gd, np, edits, flags) { +exports.applyContainerArrayChanges = function applyContainerArrayChanges(gd, np, edits, flags) { var componentType = np.astr, supplyComponentDefaults = Registry.getComponentMethod(componentType, 'supplyLayoutDefaults'), draw = Registry.getComponentMethod(componentType, 'draw'), diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 36f449b43a7..7e4aa88a064 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -1564,7 +1564,7 @@ function _restyle(gd, aobj, _traces) { helpers.swapXYData(cont); } else if(Plots.dataArrayContainers.indexOf(param.parts[0]) !== -1) { - // TODO: use manageArrays.editContainerArray here too + // TODO: use manageArrays.applyContainerArrayChanges here too helpers.manageArrayContainers(param, newVal, undoit); flags.docalc = true; } @@ -2017,7 +2017,7 @@ function _relayout(gd, aobj) { flags.docalc = true; } - // prepare the edits object we'll send to editContainerArray + // prepare the edits object we'll send to applyContainerArrayChanges if(!arrayEdits[arrayStr]) arrayEdits[arrayStr] = {}; var objEdits = arrayEdits[arrayStr][i]; if(!objEdits) objEdits = arrayEdits[arrayStr][i] = {}; @@ -2097,7 +2097,7 @@ function _relayout(gd, aobj) { // now we've collected component edits - execute them all together for(arrayStr in arrayEdits) { - var finished = manageArrays.editContainerArray(gd, + var finished = manageArrays.applyContainerArrayChanges(gd, Lib.nestedProperty(layout, arrayStr), arrayEdits[arrayStr], flags); if(!finished) flags.doplot = true; } From 3ef1c9b218bd6e7ba16104e5a67f58368600064f Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 24 Feb 2017 16:55:00 -0500 Subject: [PATCH 18/18] test errors from parent/child simultaneous edits --- test/jasmine/tests/plot_api_test.js | 78 ++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index 19419f7cb06..e46a661927a 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -180,6 +180,42 @@ describe('Test plot api', function() { .then(done); }); + it('errors if child and parent are edited together', function(done) { + var edit1 = {rando: [{a: 1}, {b: 2}]}; + var edit2 = {'rando[1]': {c: 3}}; + var edit3 = {'rando[1].d': 4}; + + Plotly.plot(gd, [{ x: [1, 2, 3], y: [1, 2, 3] }]) + .then(function() { + return Plotly.relayout(gd, edit1); + }) + .then(function() { + expect(gd.layout.rando).toEqual([{a: 1}, {b: 2}]); + return Plotly.relayout(gd, edit2); + }) + .then(function() { + expect(gd.layout.rando).toEqual([{a: 1}, {c: 3}]); + return Plotly.relayout(gd, edit3); + }) + .then(function() { + expect(gd.layout.rando).toEqual([{a: 1}, {c: 3, d: 4}]); + + // OK, setup is done - test the failing combinations + [[edit1, edit2], [edit1, edit3], [edit2, edit3]].forEach(function(v) { + // combine properties in both orders - which results in the same object + // but the properties are iterated in opposite orders + expect(function() { + return Plotly.relayout(gd, Lib.extendFlat({}, v[0], v[1])); + }).toThrow(); + expect(function() { + return Plotly.relayout(gd, Lib.extendFlat({}, v[1], v[0])); + }).toThrow(); + }); + }) + .catch(fail) + .then(done); + }); + it('can set empty text nodes', function(done) { var data = [{ x: [1, 2, 3], @@ -333,7 +369,7 @@ describe('Test plot api', function() { destroyGraphDiv(); }); - it('should redo auto z/contour when editing z array', function() { + it('should redo auto z/contour when editing z array', function(done) { Plotly.plot(gd, [{type: 'contour', z: [[1, 2], [3, 4]]}]).then(function() { expect(gd.data[0].zauto).toBe(true, gd.data[0]); expect(gd.data[0].zmin).toBe(1); @@ -348,7 +384,45 @@ describe('Test plot api', function() { expect(gd.data[0].zmax).toBe(10); expect(gd.data[0].contours).toEqual({start: 3, end: 9, size: 1}); - }); + }) + .catch(fail) + .then(done); + }); + + it('errors if child and parent are edited together', function(done) { + var edit1 = {rando: [[{a: 1}, {b: 2}]]}; + var edit2 = {'rando[1]': {c: 3}}; + var edit3 = {'rando[1].d': 4}; + + Plotly.plot(gd, [{x: [1, 2, 3], y: [1, 2, 3], type: 'scatter'}]) + .then(function() { + return Plotly.restyle(gd, edit1); + }) + .then(function() { + expect(gd.data[0].rando).toEqual([{a: 1}, {b: 2}]); + return Plotly.restyle(gd, edit2); + }) + .then(function() { + expect(gd.data[0].rando).toEqual([{a: 1}, {c: 3}]); + return Plotly.restyle(gd, edit3); + }) + .then(function() { + expect(gd.data[0].rando).toEqual([{a: 1}, {c: 3, d: 4}]); + + // OK, setup is done - test the failing combinations + [[edit1, edit2], [edit1, edit3], [edit2, edit3]].forEach(function(v) { + // combine properties in both orders - which results in the same object + // but the properties are iterated in opposite orders + expect(function() { + return Plotly.restyle(gd, Lib.extendFlat({}, v[0], v[1])); + }).toThrow(); + expect(function() { + return Plotly.restyle(gd, Lib.extendFlat({}, v[1], v[0])); + }).toThrow(); + }); + }) + .catch(fail) + .then(done); }); });