From b8950c5d8ce9ecdd0c21094c1eb26efb0e42268b Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 19 Jun 2018 10:57:42 -0400 Subject: [PATCH 01/24] fix devtools timeit sorting --- devtools/test_dashboard/perf.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devtools/test_dashboard/perf.js b/devtools/test_dashboard/perf.js index e338e14f77b..66e19a7278e 100644 --- a/devtools/test_dashboard/perf.js +++ b/devtools/test_dashboard/perf.js @@ -39,7 +39,7 @@ window.timeit = function(f, n, nchunk, arg) { var first = (times[0]).toFixed(4); var last = (times[n - 1]).toFixed(4); - times.sort(); + times.sort(function(a, b) { return a - b; }); var min = (times[0]).toFixed(4); var max = (times[n - 1]).toFixed(4); var median = (times[Math.min(Math.ceil(n / 2), n - 1)]).toFixed(4); From 355fb09126ff692bfa2025aa6f4fb52f51220194 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 18 Jun 2018 14:52:35 -0400 Subject: [PATCH 02/24] stop pushing tickmode back to axis in for a particular edge case --- src/plots/cartesian/tick_value_defaults.js | 15 ++++++++------- test/jasmine/tests/axes_test.js | 6 ++++++ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/plots/cartesian/tick_value_defaults.js b/src/plots/cartesian/tick_value_defaults.js index 907555fa8f7..ca3e1aa356d 100644 --- a/src/plots/cartesian/tick_value_defaults.js +++ b/src/plots/cartesian/tick_value_defaults.js @@ -15,18 +15,19 @@ var ONEDAY = require('../../constants/numerical').ONEDAY; module.exports = function handleTickValueDefaults(containerIn, containerOut, coerce, axType) { - var tickmodeDefault = 'auto'; + var tickmode; if(containerIn.tickmode === 'array' && (axType === 'log' || axType === 'date')) { - containerIn.tickmode = 'auto'; + tickmode = containerOut.tickmode = 'auto'; } - - if(Array.isArray(containerIn.tickvals)) tickmodeDefault = 'array'; - else if(containerIn.dtick) { - tickmodeDefault = 'linear'; + else { + var tickmodeDefault = + Array.isArray(containerIn.tickvals) ? 'array' : + containerIn.dtick ? 'linear' : + 'auto'; + tickmode = coerce('tickmode', tickmodeDefault); } - var tickmode = coerce('tickmode', tickmodeDefault); if(tickmode === 'auto') coerce('nticks'); else if(tickmode === 'linear') { diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 5f306a750b9..e987924bb33 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -1073,26 +1073,32 @@ describe('Test axes', function() { axOut = {}; mockSupplyDefaults(axIn, axOut, 'linear'); expect(axOut.tickmode).toBe('auto'); + // and not push it back to axIn (which we used to do) + expect(axIn.tickmode).toBeUndefined(); axIn = {tickmode: 'array', tickvals: 'stuff'}; axOut = {}; mockSupplyDefaults(axIn, axOut, 'linear'); expect(axOut.tickmode).toBe('auto'); + expect(axIn.tickmode).toBe('array'); axIn = {tickmode: 'array', tickvals: [1, 2, 3]}; axOut = {}; mockSupplyDefaults(axIn, axOut, 'date'); expect(axOut.tickmode).toBe('auto'); + expect(axIn.tickmode).toBe('array'); axIn = {tickvals: [1, 2, 3]}; axOut = {}; mockSupplyDefaults(axIn, axOut, 'linear'); expect(axOut.tickmode).toBe('array'); + expect(axIn.tickmode).toBeUndefined(); axIn = {dtick: 1}; axOut = {}; mockSupplyDefaults(axIn, axOut, 'linear'); expect(axOut.tickmode).toBe('linear'); + expect(axIn.tickmode).toBeUndefined(); }); it('should set nticks iff tickmode=auto', function() { From 8ddcfd6f4e90251bca98774da60438ba4ce52587 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 21 Jun 2018 14:25:52 -0400 Subject: [PATCH 03/24] minor simplification in pie defaults --- src/traces/pie/defaults.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/traces/pie/defaults.js b/src/traces/pie/defaults.js index 46068e92717..a19b5e097c2 100644 --- a/src/traces/pie/defaults.js +++ b/src/traces/pie/defaults.js @@ -27,13 +27,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout len = labels.length; if(hasVals) len = Math.min(len, vals.length); } - if(!Array.isArray(labels)) { - if(!hasVals) { - // must have at least one of vals or labels - traceOut.visible = false; - return; - } - + else if(hasVals) { len = vals.length; coerce('label0'); From b5ccfbe10a2d9f4c53a822d6d2495f6961cba6e4 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 21 Jun 2018 14:52:32 -0400 Subject: [PATCH 04/24] combine annotation defaults files & shape defaults files --- .../annotations/annotation_defaults.js | 95 -------------- src/components/annotations/defaults.js | 88 ++++++++++++- src/components/shapes/defaults.js | 111 +++++++++++++++- src/components/shapes/shape_defaults.js | 119 ------------------ 4 files changed, 189 insertions(+), 224 deletions(-) delete mode 100644 src/components/annotations/annotation_defaults.js delete mode 100644 src/components/shapes/shape_defaults.js diff --git a/src/components/annotations/annotation_defaults.js b/src/components/annotations/annotation_defaults.js deleted file mode 100644 index eba616d7923..00000000000 --- a/src/components/annotations/annotation_defaults.js +++ /dev/null @@ -1,95 +0,0 @@ -/** -* Copyright 2012-2018, 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 Axes = require('../../plots/cartesian/axes'); -var handleAnnotationCommonDefaults = require('./common_defaults'); -var attributes = require('./attributes'); - - -module.exports = function handleAnnotationDefaults(annIn, annOut, fullLayout, opts, itemOpts) { - opts = opts || {}; - itemOpts = itemOpts || {}; - - function coerce(attr, dflt) { - return Lib.coerce(annIn, annOut, attributes, attr, dflt); - } - - var visible = coerce('visible', !itemOpts.itemIsNotPlainObject); - var clickToShow = coerce('clicktoshow'); - - if(!(visible || clickToShow)) return annOut; - - handleAnnotationCommonDefaults(annIn, annOut, fullLayout, coerce); - - var showArrow = annOut.showarrow; - - // positioning - var axLetters = ['x', 'y'], - arrowPosDflt = [-10, -30], - gdMock = {_fullLayout: fullLayout}; - for(var i = 0; i < 2; i++) { - var axLetter = axLetters[i]; - - // xref, yref - var axRef = Axes.coerceRef(annIn, annOut, gdMock, axLetter, '', 'paper'); - - // x, y - Axes.coercePosition(annOut, gdMock, coerce, axRef, axLetter, 0.5); - - if(showArrow) { - var arrowPosAttr = 'a' + axLetter, - // axref, ayref - aaxRef = Axes.coerceRef(annIn, annOut, gdMock, arrowPosAttr, 'pixel'); - - // for now the arrow can only be on the same axis or specified as pixels - // TODO: sometime it might be interesting to allow it to be on *any* axis - // but that would require updates to drawing & autorange code and maybe more - if(aaxRef !== 'pixel' && aaxRef !== axRef) { - aaxRef = annOut[arrowPosAttr] = 'pixel'; - } - - // ax, ay - var aDflt = (aaxRef === 'pixel') ? arrowPosDflt[i] : 0.4; - Axes.coercePosition(annOut, gdMock, coerce, aaxRef, arrowPosAttr, aDflt); - } - - // xanchor, yanchor - coerce(axLetter + 'anchor'); - - // xshift, yshift - coerce(axLetter + 'shift'); - } - - // if you have one coordinate you should have both - Lib.noneOrAll(annIn, annOut, ['x', 'y']); - - // if you have one part of arrow length you should have both - if(showArrow) { - Lib.noneOrAll(annIn, annOut, ['ax', 'ay']); - } - - if(clickToShow) { - var xClick = coerce('xclick'); - var yClick = coerce('yclick'); - - // put the actual click data to bind to into private attributes - // so we don't have to do this little bit of logic on every hover event - annOut._xclick = (xClick === undefined) ? - annOut.x : - Axes.cleanPosition(xClick, gdMock, annOut.xref); - annOut._yclick = (yClick === undefined) ? - annOut.y : - Axes.cleanPosition(yClick, gdMock, annOut.yref); - } - - return annOut; -}; diff --git a/src/components/annotations/defaults.js b/src/components/annotations/defaults.js index c101813501f..07ad6ae4c5a 100644 --- a/src/components/annotations/defaults.js +++ b/src/components/annotations/defaults.js @@ -9,15 +9,93 @@ 'use strict'; +var Lib = require('../../lib'); +var Axes = require('../../plots/cartesian/axes'); var handleArrayContainerDefaults = require('../../plots/array_container_defaults'); -var handleAnnotationDefaults = require('./annotation_defaults'); + +var handleAnnotationCommonDefaults = require('./common_defaults'); +var attributes = require('./attributes'); module.exports = function supplyLayoutDefaults(layoutIn, layoutOut) { - var opts = { + handleArrayContainerDefaults(layoutIn, layoutOut, { name: 'annotations', handleItemDefaults: handleAnnotationDefaults - }; - - handleArrayContainerDefaults(layoutIn, layoutOut, opts); + }); }; + +function handleAnnotationDefaults(annIn, annOut, fullLayout, opts, itemOpts) { + function coerce(attr, dflt) { + return Lib.coerce(annIn, annOut, attributes, attr, dflt); + } + + var visible = coerce('visible', !itemOpts.itemIsNotPlainObject); + var clickToShow = coerce('clicktoshow'); + + if(!(visible || clickToShow)) return annOut; + + handleAnnotationCommonDefaults(annIn, annOut, fullLayout, coerce); + + var showArrow = annOut.showarrow; + + // positioning + var axLetters = ['x', 'y'], + arrowPosDflt = [-10, -30], + gdMock = {_fullLayout: fullLayout}; + for(var i = 0; i < 2; i++) { + var axLetter = axLetters[i]; + + // xref, yref + var axRef = Axes.coerceRef(annIn, annOut, gdMock, axLetter, '', 'paper'); + + // x, y + Axes.coercePosition(annOut, gdMock, coerce, axRef, axLetter, 0.5); + + if(showArrow) { + var arrowPosAttr = 'a' + axLetter, + // axref, ayref + aaxRef = Axes.coerceRef(annIn, annOut, gdMock, arrowPosAttr, 'pixel'); + + // for now the arrow can only be on the same axis or specified as pixels + // TODO: sometime it might be interesting to allow it to be on *any* axis + // but that would require updates to drawing & autorange code and maybe more + if(aaxRef !== 'pixel' && aaxRef !== axRef) { + aaxRef = annOut[arrowPosAttr] = 'pixel'; + } + + // ax, ay + var aDflt = (aaxRef === 'pixel') ? arrowPosDflt[i] : 0.4; + Axes.coercePosition(annOut, gdMock, coerce, aaxRef, arrowPosAttr, aDflt); + } + + // xanchor, yanchor + coerce(axLetter + 'anchor'); + + // xshift, yshift + coerce(axLetter + 'shift'); + } + + // if you have one coordinate you should have both + Lib.noneOrAll(annIn, annOut, ['x', 'y']); + + // if you have one part of arrow length you should have both + if(showArrow) { + Lib.noneOrAll(annIn, annOut, ['ax', 'ay']); + } + + if(clickToShow) { + var xClick = coerce('xclick'); + var yClick = coerce('yclick'); + + // put the actual click data to bind to into private attributes + // so we don't have to do this little bit of logic on every hover event + annOut._xclick = (xClick === undefined) ? + annOut.x : + Axes.cleanPosition(xClick, gdMock, annOut.xref); + annOut._yclick = (yClick === undefined) ? + annOut.y : + Axes.cleanPosition(yClick, gdMock, annOut.yref); + } + + return annOut; +} diff --git a/src/components/shapes/defaults.js b/src/components/shapes/defaults.js index bcb010760fd..f685a3eccc5 100644 --- a/src/components/shapes/defaults.js +++ b/src/components/shapes/defaults.js @@ -9,15 +9,116 @@ 'use strict'; +var Lib = require('../../lib'); +var Axes = require('../../plots/cartesian/axes'); var handleArrayContainerDefaults = require('../../plots/array_container_defaults'); -var handleShapeDefaults = require('./shape_defaults'); + +var attributes = require('./attributes'); +var helpers = require('./helpers'); module.exports = function supplyLayoutDefaults(layoutIn, layoutOut) { - var opts = { + handleArrayContainerDefaults(layoutIn, layoutOut, { name: 'shapes', handleItemDefaults: handleShapeDefaults - }; - - handleArrayContainerDefaults(layoutIn, layoutOut, opts); + }); }; + +function handleShapeDefaults(shapeIn, shapeOut, fullLayout, opts, itemOpts) { + function coerce(attr, dflt) { + return Lib.coerce(shapeIn, shapeOut, attributes, attr, dflt); + } + + var visible = coerce('visible', !itemOpts.itemIsNotPlainObject); + + if(!visible) return shapeOut; + + coerce('layer'); + coerce('opacity'); + coerce('fillcolor'); + coerce('line.color'); + coerce('line.width'); + coerce('line.dash'); + + var dfltType = shapeIn.path ? 'path' : 'rect', + shapeType = coerce('type', dfltType), + xSizeMode = coerce('xsizemode'), + ySizeMode = coerce('ysizemode'); + + // positioning + var axLetters = ['x', 'y']; + for(var i = 0; i < 2; i++) { + var axLetter = axLetters[i], + attrAnchor = axLetter + 'anchor', + sizeMode = axLetter === 'x' ? xSizeMode : ySizeMode, + gdMock = {_fullLayout: fullLayout}, + ax, + pos2r, + r2pos; + + // xref, yref + var axRef = Axes.coerceRef(shapeIn, shapeOut, gdMock, axLetter, '', 'paper'); + + if(axRef !== 'paper') { + ax = Axes.getFromId(gdMock, axRef); + r2pos = helpers.rangeToShapePosition(ax); + pos2r = helpers.shapePositionToRange(ax); + } + else { + pos2r = r2pos = Lib.identity; + } + + // Coerce x0, x1, y0, y1 + if(shapeType !== 'path') { + var dflt0 = 0.25, + dflt1 = 0.75; + + // hack until V2.0 when log has regular range behavior - make it look like other + // ranges to send to coerce, then put it back after + // this is all to give reasonable default position behavior on log axes, which is + // a pretty unimportant edge case so we could just ignore this. + var attr0 = axLetter + '0', + attr1 = axLetter + '1', + in0 = shapeIn[attr0], + in1 = shapeIn[attr1]; + shapeIn[attr0] = pos2r(shapeIn[attr0], true); + shapeIn[attr1] = pos2r(shapeIn[attr1], true); + + if(sizeMode === 'pixel') { + coerce(attr0, 0); + coerce(attr1, 10); + } else { + Axes.coercePosition(shapeOut, gdMock, coerce, axRef, attr0, dflt0); + Axes.coercePosition(shapeOut, gdMock, coerce, axRef, attr1, dflt1); + } + + // hack part 2 + shapeOut[attr0] = r2pos(shapeOut[attr0]); + shapeOut[attr1] = r2pos(shapeOut[attr1]); + shapeIn[attr0] = in0; + shapeIn[attr1] = in1; + } + + // Coerce xanchor and yanchor + if(sizeMode === 'pixel') { + // Hack for log axis described above + var inAnchor = shapeIn[attrAnchor]; + shapeIn[attrAnchor] = pos2r(shapeIn[attrAnchor], true); + + Axes.coercePosition(shapeOut, gdMock, coerce, axRef, attrAnchor, 0.25); + + // Hack part 2 + shapeOut[attrAnchor] = r2pos(shapeOut[attrAnchor]); + shapeIn[attrAnchor] = inAnchor; + } + } + + if(shapeType === 'path') { + coerce('path'); + } + else { + Lib.noneOrAll(shapeIn, shapeOut, ['x0', 'x1', 'y0', 'y1']); + } + + return shapeOut; +} diff --git a/src/components/shapes/shape_defaults.js b/src/components/shapes/shape_defaults.js deleted file mode 100644 index 66259ac63d7..00000000000 --- a/src/components/shapes/shape_defaults.js +++ /dev/null @@ -1,119 +0,0 @@ -/** -* Copyright 2012-2018, 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 Axes = require('../../plots/cartesian/axes'); - -var attributes = require('./attributes'); -var helpers = require('./helpers'); - - -module.exports = function handleShapeDefaults(shapeIn, shapeOut, fullLayout, opts, itemOpts) { - opts = opts || {}; - itemOpts = itemOpts || {}; - - function coerce(attr, dflt) { - return Lib.coerce(shapeIn, shapeOut, attributes, attr, dflt); - } - - var visible = coerce('visible', !itemOpts.itemIsNotPlainObject); - - if(!visible) return shapeOut; - - coerce('layer'); - coerce('opacity'); - coerce('fillcolor'); - coerce('line.color'); - coerce('line.width'); - coerce('line.dash'); - - var dfltType = shapeIn.path ? 'path' : 'rect', - shapeType = coerce('type', dfltType), - xSizeMode = coerce('xsizemode'), - ySizeMode = coerce('ysizemode'); - - // positioning - var axLetters = ['x', 'y']; - for(var i = 0; i < 2; i++) { - var axLetter = axLetters[i], - attrAnchor = axLetter + 'anchor', - sizeMode = axLetter === 'x' ? xSizeMode : ySizeMode, - gdMock = {_fullLayout: fullLayout}, - ax, - pos2r, - r2pos; - - // xref, yref - var axRef = Axes.coerceRef(shapeIn, shapeOut, gdMock, axLetter, '', 'paper'); - - if(axRef !== 'paper') { - ax = Axes.getFromId(gdMock, axRef); - r2pos = helpers.rangeToShapePosition(ax); - pos2r = helpers.shapePositionToRange(ax); - } - else { - pos2r = r2pos = Lib.identity; - } - - // Coerce x0, x1, y0, y1 - if(shapeType !== 'path') { - var dflt0 = 0.25, - dflt1 = 0.75; - - // hack until V2.0 when log has regular range behavior - make it look like other - // ranges to send to coerce, then put it back after - // this is all to give reasonable default position behavior on log axes, which is - // a pretty unimportant edge case so we could just ignore this. - var attr0 = axLetter + '0', - attr1 = axLetter + '1', - in0 = shapeIn[attr0], - in1 = shapeIn[attr1]; - shapeIn[attr0] = pos2r(shapeIn[attr0], true); - shapeIn[attr1] = pos2r(shapeIn[attr1], true); - - if(sizeMode === 'pixel') { - coerce(attr0, 0); - coerce(attr1, 10); - } else { - Axes.coercePosition(shapeOut, gdMock, coerce, axRef, attr0, dflt0); - Axes.coercePosition(shapeOut, gdMock, coerce, axRef, attr1, dflt1); - } - - // hack part 2 - shapeOut[attr0] = r2pos(shapeOut[attr0]); - shapeOut[attr1] = r2pos(shapeOut[attr1]); - shapeIn[attr0] = in0; - shapeIn[attr1] = in1; - } - - // Coerce xanchor and yanchor - if(sizeMode === 'pixel') { - // Hack for log axis described above - var inAnchor = shapeIn[attrAnchor]; - shapeIn[attrAnchor] = pos2r(shapeIn[attrAnchor], true); - - Axes.coercePosition(shapeOut, gdMock, coerce, axRef, attrAnchor, 0.25); - - // Hack part 2 - shapeOut[attrAnchor] = r2pos(shapeOut[attrAnchor]); - shapeIn[attrAnchor] = inAnchor; - } - } - - if(shapeType === 'path') { - coerce('path'); - } - else { - Lib.noneOrAll(shapeIn, shapeOut, ['x0', 'x1', 'y0', 'y1']); - } - - return shapeOut; -}; From 86e09bb293b16b27982cba4a94ceebef66ef7829 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 20 Jun 2018 16:33:45 -0400 Subject: [PATCH 05/24] add visible attribute to lots of container array items: rangeselector.buttons updatemenus.buttons slider.steps axes.tickformatstops mapbox.layers --- .../rangeselector/button_attributes.js | 6 + src/components/rangeselector/defaults.js | 29 +-- src/components/rangeselector/draw.js | 2 +- src/components/sliders/attributes.js | 8 + src/components/sliders/defaults.js | 61 +++---- src/components/sliders/draw.js | 49 +++--- src/components/updatemenus/attributes.js | 5 + src/components/updatemenus/defaults.js | 15 +- src/components/updatemenus/draw.js | 22 +-- src/plots/array_container_defaults.js | 7 +- src/plots/cartesian/axes.js | 12 +- src/plots/cartesian/layout_attributes.js | 10 ++ src/plots/cartesian/tick_label_defaults.js | 26 ++- src/plots/mapbox/layers.js | 2 +- src/plots/mapbox/layout_attributes.js | 8 + src/plots/mapbox/layout_defaults.js | 24 +-- test/jasmine/tests/axes_test.js | 30 +++- test/jasmine/tests/mapbox_test.js | 31 +++- test/jasmine/tests/range_selector_test.js | 47 +++-- test/jasmine/tests/sliders_test.js | 87 +++++++-- test/jasmine/tests/updatemenus_test.js | 166 ++++++++++++------ 21 files changed, 426 insertions(+), 221 deletions(-) diff --git a/src/components/rangeselector/button_attributes.js b/src/components/rangeselector/button_attributes.js index bdc3d6f86d8..fff7645e2d5 100644 --- a/src/components/rangeselector/button_attributes.js +++ b/src/components/rangeselector/button_attributes.js @@ -10,6 +10,12 @@ module.exports = { + visible: { + valType: 'boolean', + role: 'info', + editType: 'plot', + description: 'Determines whether or not this button is visible.' + }, step: { valType: 'enumerated', role: 'info', diff --git a/src/components/rangeselector/defaults.js b/src/components/rangeselector/defaults.js index 741dfacb5d1..1e3624f2870 100644 --- a/src/components/rangeselector/defaults.js +++ b/src/components/rangeselector/defaults.js @@ -10,6 +10,7 @@ var Lib = require('../../lib'); var Color = require('../color'); +var Template = require('../../plot_api/plot_template'); var attributes = require('./attributes'); var buttonAttrs = require('./button_attributes'); @@ -17,8 +18,8 @@ var constants = require('./constants'); module.exports = function handleDefaults(containerIn, containerOut, layout, counterAxes, calendar) { - var selectorIn = containerIn.rangeselector || {}, - selectorOut = containerOut.rangeselector = {}; + var selectorIn = containerIn.rangeselector || {}; + var selectorOut = Template.newContainer(containerOut, 'rangeselector'); function coerce(attr, dflt) { return Lib.coerce(selectorIn, selectorOut, attributes, attr, dflt); @@ -59,22 +60,24 @@ function buttonsDefaults(containerIn, containerOut, calendar) { buttonIn = buttonsIn[i]; buttonOut = {}; - if(!Lib.isPlainObject(buttonIn)) continue; + var visible = coerce('visible', Lib.isPlainObject(buttonIn)); - var step = coerce('step'); - if(step !== 'all') { - if(calendar && calendar !== 'gregorian' && (step === 'month' || step === 'year')) { - buttonOut.stepmode = 'backward'; - } - else { - coerce('stepmode'); + if(visible) { + var step = coerce('step'); + if(step !== 'all') { + if(calendar && calendar !== 'gregorian' && (step === 'month' || step === 'year')) { + buttonOut.stepmode = 'backward'; + } + else { + coerce('stepmode'); + } + + coerce('count'); } - coerce('count'); + coerce('label'); } - coerce('label'); - buttonOut._index = i; buttonsOut.push(buttonOut); } diff --git a/src/components/rangeselector/draw.js b/src/components/rangeselector/draw.js index b21f73f6a08..aa717d663b5 100644 --- a/src/components/rangeselector/draw.js +++ b/src/components/rangeselector/draw.js @@ -50,7 +50,7 @@ module.exports = function draw(gd) { selectorLayout = axisLayout.rangeselector; var buttons = selector.selectAll('g.button') - .data(selectorLayout.buttons); + .data(Lib.filterVisible(selectorLayout.buttons)); buttons.enter().append('g') .classed('button', true); diff --git a/src/components/sliders/attributes.js b/src/components/sliders/attributes.js index 3edfee97767..1ab099589c3 100644 --- a/src/components/sliders/attributes.js +++ b/src/components/sliders/attributes.js @@ -18,6 +18,14 @@ var constants = require('./constants'); var stepsAttrs = { _isLinkedToArray: 'step', + visible: { + valType: 'boolean', + role: 'info', + dflt: true, + description: [ + 'Determines whether or not this step is included in the slider.' + ].join(' ') + }, method: { valType: 'enumerated', values: ['restyle', 'relayout', 'animate', 'update', 'skip'], diff --git a/src/components/sliders/defaults.js b/src/components/sliders/defaults.js index 42fe3aa8345..af3faad03fd 100644 --- a/src/components/sliders/defaults.js +++ b/src/components/sliders/defaults.js @@ -19,12 +19,10 @@ var stepAttrs = attributes.steps; module.exports = function slidersDefaults(layoutIn, layoutOut) { - var opts = { + handleArrayContainerDefaults(layoutIn, layoutOut, { name: name, handleItemDefaults: sliderDefaults - }; - - handleArrayContainerDefaults(layoutIn, layoutOut, opts); + }); }; function sliderDefaults(sliderIn, sliderOut, layoutOut) { @@ -33,12 +31,27 @@ function sliderDefaults(sliderIn, sliderOut, layoutOut) { return Lib.coerce(sliderIn, sliderOut, attributes, attr, dflt); } - var steps = stepsDefaults(sliderIn, sliderOut); + var steps = handleArrayContainerDefaults(sliderIn, sliderOut, { + name: 'steps', + handleItemDefaults: stepDefaults + }); + + var stepCount = 0; + for(var i = 0; i < steps.length; i++) { + if(steps[i].visible) stepCount++; + } - var visible = coerce('visible', steps.length > 0); + var visible; + // If it has fewer than two options, it's not really a slider + if(stepCount < 2) visible = sliderOut.visible = false; + else visible = coerce('visible'); if(!visible) return; - coerce('active'); + sliderOut._stepCount = stepCount; + var visSteps = sliderOut._visibleSteps = Lib.filterVisible(steps); + + var active = coerce('active'); + if(!(steps[active] || {}).visible) sliderOut.active = visSteps[0]._index; coerce('x'); coerce('y'); @@ -81,33 +94,21 @@ function sliderDefaults(sliderIn, sliderOut, layoutOut) { coerce('minorticklen'); } -function stepsDefaults(sliderIn, sliderOut) { - var valuesIn = sliderIn.steps || [], - valuesOut = sliderOut.steps = []; - - var valueIn, valueOut; - +function stepDefaults(valueIn, valueOut, sliderOut, opts, itemOpts) { function coerce(attr, dflt) { return Lib.coerce(valueIn, valueOut, stepAttrs, attr, dflt); } - for(var i = 0; i < valuesIn.length; i++) { - valueIn = valuesIn[i]; - valueOut = {}; - - coerce('method'); - - if(!Lib.isPlainObject(valueIn) || (valueOut.method !== 'skip' && !Array.isArray(valueIn.args))) { - continue; - } - - coerce('args'); - coerce('label', 'step-' + i); - coerce('value', valueOut.label); - coerce('execute'); - - valuesOut.push(valueOut); + var visible; + if(itemOpts.itemIsNotPlainObject || (valueIn.method !== 'skip' && !Array.isArray(valueIn.args))) { + visible = valueOut.visible = false; } + else visible = coerce('visible'); + if(!visible) return; - return valuesOut; + coerce('method'); + coerce('args'); + var label = coerce('label', 'step-' + itemOpts.index); + coerce('value', label); + coerce('execute'); } diff --git a/src/components/sliders/draw.js b/src/components/sliders/draw.js index 512f7fef9fc..48b1518605c 100644 --- a/src/components/sliders/draw.js +++ b/src/components/sliders/draw.js @@ -74,14 +74,11 @@ module.exports = function draw(gd) { } sliderGroups.each(function(sliderOpts) { - // If it has fewer than two options, it's not really a slider: - if(sliderOpts.steps.length < 2) return; - var gSlider = d3.select(this); computeLabelSteps(sliderOpts); - Plots.manageCommandObserver(gd, sliderOpts, sliderOpts.steps, function(data) { + Plots.manageCommandObserver(gd, sliderOpts, sliderOpts._visibleSteps, function(data) { // NB: Same as below. This is *not* always the same as sliderOpts since // if a new set of steps comes in, the reference in this callback would // be invalid. We need to refetch it from the slider group, which is @@ -111,7 +108,7 @@ function makeSliderData(fullLayout, gd) { for(var i = 0; i < contOpts.length; i++) { var item = contOpts[i]; - if(!item.visible || !item.steps.length) continue; + if(!item.visible) continue; item._gd = gd; sliderData.push(item); } @@ -127,7 +124,7 @@ function keyFunction(opts) { // Compute the dimensions (mutates sliderOpts): function findDimensions(gd, sliderOpts) { var sliderLabels = Drawing.tester.selectAll('g.' + constants.labelGroupClass) - .data(sliderOpts.steps); + .data(sliderOpts._visibleSteps); sliderLabels.enter().append('g') .classed(constants.labelGroupClass, true); @@ -176,7 +173,7 @@ function findDimensions(gd, sliderOpts) { dims.inputAreaLength = Math.round(dims.outerLength - sliderOpts.pad.l - sliderOpts.pad.r); var textableInputLength = dims.inputAreaLength - 2 * constants.stepInset; - var availableSpacePerLabel = textableInputLength / (sliderOpts.steps.length - 1); + var availableSpacePerLabel = textableInputLength / (sliderOpts._stepCount - 1); var computedSpacePerLabel = maxLabelWidth + constants.labelPadding; dims.labelStride = Math.max(1, Math.ceil(computedSpacePerLabel / availableSpacePerLabel)); dims.labelHeight = labelHeight; @@ -260,8 +257,8 @@ function drawSlider(gd, sliderGroup, sliderOpts) { // the *current* slider step is removed. The drawing functions will error out // when they fail to find it, so the fix for now is that it will just draw the // slider in the first position but will not execute the command. - if(sliderOpts.active >= sliderOpts.steps.length) { - sliderOpts.active = 0; + if(!((sliderOpts.steps[sliderOpts.active] || {}).visible)) { + sliderOpts.active = sliderOpts._visibleSteps[0]._index; } // These are carefully ordered for proper z-ordering: @@ -278,7 +275,7 @@ function drawSlider(gd, sliderGroup, sliderOpts) { // Position the rectangle: Drawing.setTranslate(sliderGroup, dims.lx + sliderOpts.pad.l, dims.ly + sliderOpts.pad.t); - sliderGroup.call(setGripPosition, sliderOpts, sliderOpts.active / (sliderOpts.steps.length - 1), false); + sliderGroup.call(setGripPosition, sliderOpts, false); sliderGroup.call(drawCurrentValue, sliderOpts); } @@ -406,10 +403,11 @@ function drawLabelGroup(sliderGroup, sliderOpts) { } function handleInput(gd, sliderGroup, sliderOpts, normalizedPosition, doTransition) { - var quantizedPosition = Math.round(normalizedPosition * (sliderOpts.steps.length - 1)); + var quantizedPosition = Math.round(normalizedPosition * (sliderOpts._stepCount - 1)); + var quantizedIndex = sliderOpts._visibleSteps[quantizedPosition]._index; - if(quantizedPosition !== sliderOpts.active) { - setActive(gd, sliderGroup, sliderOpts, quantizedPosition, true, doTransition); + if(quantizedIndex !== sliderOpts.active) { + setActive(gd, sliderGroup, sliderOpts, quantizedIndex, true, doTransition); } } @@ -419,7 +417,7 @@ function setActive(gd, sliderGroup, sliderOpts, index, doCallback, doTransition) var step = sliderOpts.steps[sliderOpts.active]; - sliderGroup.call(setGripPosition, sliderOpts, sliderOpts.active / (sliderOpts.steps.length - 1), doTransition); + sliderGroup.call(setGripPosition, sliderOpts, doTransition); sliderGroup.call(drawCurrentValue, sliderOpts); gd.emit('plotly_sliderchange', { @@ -502,7 +500,7 @@ function attachGripEvents(item, gd, sliderGroup) { function drawTicks(sliderGroup, sliderOpts) { var tick = sliderGroup.selectAll('rect.' + constants.tickRectClass) - .data(sliderOpts.steps); + .data(sliderOpts._visibleSteps); var dims = sliderOpts._dims; tick.enter().append('rect') @@ -524,7 +522,7 @@ function drawTicks(sliderGroup, sliderOpts) { .call(Color.fill, isMajor ? sliderOpts.tickcolor : sliderOpts.tickcolor); Drawing.setTranslate(item, - normalizedValueToPosition(sliderOpts, i / (sliderOpts.steps.length - 1)) - 0.5 * sliderOpts.tickwidth, + normalizedValueToPosition(sliderOpts, i / (sliderOpts._stepCount - 1)) - 0.5 * sliderOpts.tickwidth, (isMajor ? constants.tickOffset : constants.minorTickOffset) + dims.currentValueTotalHeight ); }); @@ -534,21 +532,28 @@ function drawTicks(sliderGroup, sliderOpts) { function computeLabelSteps(sliderOpts) { var dims = sliderOpts._dims; dims.labelSteps = []; - var i0 = 0; - var nsteps = sliderOpts.steps.length; + var nsteps = sliderOpts._stepCount; - for(var i = i0; i < nsteps; i += dims.labelStride) { + for(var i = 0; i < nsteps; i += dims.labelStride) { dims.labelSteps.push({ fraction: i / (nsteps - 1), - step: sliderOpts.steps[i] + step: sliderOpts._visibleSteps[i] }); } } -function setGripPosition(sliderGroup, sliderOpts, position, doTransition) { +function setGripPosition(sliderGroup, sliderOpts, doTransition) { var grip = sliderGroup.select('rect.' + constants.gripRectClass); - var x = normalizedValueToPosition(sliderOpts, position); + var quantizedIndex = 0; + for(var i = 0; i < sliderOpts._stepCount; i++) { + if(sliderOpts._visibleSteps[i]._index === sliderOpts.active) { + quantizedIndex = i; + break; + } + } + + var x = normalizedValueToPosition(sliderOpts, quantizedIndex / (sliderOpts._stepCount - 1)); // If this is true, then *this component* is already invoking its own command // and has triggered its own animation. diff --git a/src/components/updatemenus/attributes.js b/src/components/updatemenus/attributes.js index 0bcdede7a60..c64149b54a7 100644 --- a/src/components/updatemenus/attributes.js +++ b/src/components/updatemenus/attributes.js @@ -17,6 +17,11 @@ var padAttrs = require('../../plots/pad_attributes'); var buttonsAttrs = { _isLinkedToArray: 'button', + visible: { + valType: 'boolean', + role: 'info', + description: 'Determines whether or not this button is visible.' + }, method: { valType: 'enumerated', values: ['restyle', 'relayout', 'animate', 'update', 'skip'], diff --git a/src/components/updatemenus/defaults.js b/src/components/updatemenus/defaults.js index a3fbb0c3260..38dedd0d1c3 100644 --- a/src/components/updatemenus/defaults.js +++ b/src/components/updatemenus/defaults.js @@ -76,16 +76,15 @@ function buttonsDefaults(menuIn, menuOut) { buttonIn = buttonsIn[i]; buttonOut = {}; - coerce('method'); - - if(!Lib.isPlainObject(buttonIn) || (buttonOut.method !== 'skip' && !Array.isArray(buttonIn.args))) { - continue; + var visible = coerce('visible', Lib.isPlainObject(buttonIn) && + (buttonIn.method === 'skip' || Array.isArray(buttonIn.args))); + if(visible) { + coerce('method'); + coerce('args'); + coerce('label'); + coerce('execute'); } - coerce('args'); - coerce('label'); - coerce('execute'); - buttonOut._index = i; buttonsOut.push(buttonOut); } diff --git a/src/components/updatemenus/draw.js b/src/components/updatemenus/draw.js index e72aa49231e..49075d67b97 100644 --- a/src/components/updatemenus/draw.js +++ b/src/components/updatemenus/draw.js @@ -25,7 +25,7 @@ var ScrollBox = require('./scrollbox'); module.exports = function draw(gd) { var fullLayout = gd._fullLayout, - menuData = makeMenuData(fullLayout); + menuData = Lib.filterVisible(fullLayout[constants.name]); /* Update menu data is bound to the header-group. * The items in the header group are always present. @@ -137,22 +137,6 @@ module.exports = function draw(gd) { }); }; -/** - * get only visible menus for display - */ -function makeMenuData(fullLayout) { - var contOpts = fullLayout[constants.name]; - var menuData = []; - - for(var i = 0; i < contOpts.length; i++) { - var item = contOpts[i]; - - if(item.visible) menuData.push(item); - } - - return menuData; -} - // Note that '_index' is set at the default step, // it corresponds to the menu index in the user layout update menu container. // Because a menu can be set invisible, @@ -255,7 +239,7 @@ function drawButtons(gd, gHeader, gButton, scrollBox, menuOpts) { var klass = menuOpts.type === 'dropdown' ? constants.dropdownButtonClassName : constants.buttonClassName; var buttons = gButton.selectAll('g.' + klass) - .data(buttonData); + .data(Lib.filterVisible(buttonData)); var enter = buttons.enter().append('g') .classed(klass, true); @@ -498,7 +482,7 @@ function findDimensions(gd, menuOpts) { }; var fakeButtons = Drawing.tester.selectAll('g.' + constants.dropdownButtonClassName) - .data(menuOpts.buttons); + .data(Lib.filterVisible(menuOpts.buttons)); fakeButtons.enter().append('g') .classed(constants.dropdownButtonClassName, true); diff --git a/src/plots/array_container_defaults.js b/src/plots/array_container_defaults.js index 70983dc33d8..6c648d7bbdd 100644 --- a/src/plots/array_container_defaults.js +++ b/src/plots/array_container_defaults.js @@ -34,6 +34,7 @@ var Lib = require('../lib'); * - opts {object} (as in closure) * - itemOpts {object} * - itemIsNotPlainObject {boolean} + * - index {integer} * N.B. * * - opts is passed to handleItemDefaults so it can also store @@ -50,9 +51,9 @@ module.exports = function handleArrayContainerDefaults(parentObjIn, parentObjOut i; for(i = 0; i < contIn.length; i++) { - var itemIn = contIn[i], - itemOut = {}, - itemOpts = {}; + var itemIn = contIn[i]; + var itemOut = {}; + var itemOpts = {index: i}; if(!Lib.isPlainObject(itemIn)) { itemOpts.itemIsNotPlainObject = true; diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 5bd190fe33d..8c808ffa3d1 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -1389,14 +1389,15 @@ axes.getTickFormat = function(ax) { return (isLeftDtickNull || isDtickInRangeLeft) && (isRightDtickNull || isDtickInRangeRight); } - var tickstop; + var tickstop, stopi; if(ax.tickformatstops && ax.tickformatstops.length > 0) { switch(ax.type) { case 'date': case 'linear': { for(i = 0; i < ax.tickformatstops.length; i++) { - if(isProperStop(ax.dtick, ax.tickformatstops[i].dtickrange, convertToMs)) { - tickstop = ax.tickformatstops[i]; + stopi = ax.tickformatstops[i]; + if(stopi.visible && isProperStop(ax.dtick, stopi.dtickrange, convertToMs)) { + tickstop = stopi; break; } } @@ -1404,8 +1405,9 @@ axes.getTickFormat = function(ax) { } case 'log': { for(i = 0; i < ax.tickformatstops.length; i++) { - if(isProperLogStop(ax.dtick, ax.tickformatstops[i].dtickrange)) { - tickstop = ax.tickformatstops[i]; + stopi = ax.tickformatstops[i]; + if(stopi.visible && isProperLogStop(ax.dtick, stopi.dtickrange)) { + tickstop = stopi; break; } } diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 2fe31f5d4c8..9c3c7de77b0 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -513,6 +513,16 @@ module.exports = { tickformatstops: { _isLinkedToArray: 'tickformatstop', + visible: { + valType: 'boolean', + role: 'info', + dflt: true, + editType: 'ticks', + description: [ + 'Determines whether or not this stop is used.', + 'If `false`, this stop is ignored even within its `dtickrange`.' + ].join(' ') + }, dtickrange: { valType: 'info_array', role: 'info', diff --git a/src/plots/cartesian/tick_label_defaults.js b/src/plots/cartesian/tick_label_defaults.js index 06d3212a36d..62e594ee2c5 100644 --- a/src/plots/cartesian/tick_label_defaults.js +++ b/src/plots/cartesian/tick_label_defaults.js @@ -11,6 +11,7 @@ var Lib = require('../../lib'); var layoutAttributes = require('./layout_attributes'); +var handleArrayContainerDefaults = require('../array_container_defaults'); module.exports = function handleTickLabelDefaults(containerIn, containerOut, coerce, axType, options) { var showAttrDflt = getShowAttrDflt(containerIn); @@ -37,7 +38,13 @@ module.exports = function handleTickLabelDefaults(containerIn, containerOut, coe if(axType !== 'category') { var tickFormat = coerce('tickformat'); - tickformatstopsDefaults(containerIn, containerOut); + var tickformatStops = containerIn.tickformatstops; + if(Array.isArray(tickformatStops) && tickformatStops.length) { + handleArrayContainerDefaults(containerIn, containerOut, { + name: 'tickformatstops', + handleItemDefaults: tickformatstopDefaults + }); + } if(!tickFormat && axType !== 'date') { coerce('showexponent', showAttrDflt); coerce('exponentformat'); @@ -77,25 +84,14 @@ function getShowAttrDflt(containerIn) { } } -function tickformatstopsDefaults(tickformatIn, tickformatOut) { - var valuesIn = tickformatIn.tickformatstops; - var valuesOut = tickformatOut.tickformatstops = []; - - if(!Array.isArray(valuesIn)) return; - - var valueIn, valueOut; - +function tickformatstopDefaults(valueIn, valueOut, contOut, opts, itemOpts) { function coerce(attr, dflt) { return Lib.coerce(valueIn, valueOut, layoutAttributes.tickformatstops, attr, dflt); } - for(var i = 0; i < valuesIn.length; i++) { - valueIn = valuesIn[i]; - valueOut = {}; - + var visible = coerce('visible', !itemOpts.itemIsNotPlainObject); + if(visible) { coerce('dtickrange'); coerce('value'); - - valuesOut.push(valueOut); } } diff --git a/src/plots/mapbox/layers.js b/src/plots/mapbox/layers.js index 3262acccade..3c14483c1d1 100644 --- a/src/plots/mapbox/layers.js +++ b/src/plots/mapbox/layers.js @@ -127,7 +127,7 @@ proto.dispose = function dispose() { function isVisible(opts) { var source = opts.source; - return ( + return opts.visible && ( Lib.isPlainObject(source) || (typeof source === 'string' && source.length > 0) ); diff --git a/src/plots/mapbox/layout_attributes.js b/src/plots/mapbox/layout_attributes.js index 2306c8b4bcf..59f9f3006d1 100644 --- a/src/plots/mapbox/layout_attributes.js +++ b/src/plots/mapbox/layout_attributes.js @@ -91,6 +91,14 @@ module.exports = overrideAll({ layers: { _isLinkedToArray: 'layer', + visible: { + valType: 'boolean', + role: 'info', + dflt: true, + description: [ + 'Determines whether this layer is displayed' + ].join(' ') + }, sourcetype: { valType: 'enumerated', values: ['geojson', 'vector'], diff --git a/src/plots/mapbox/layout_defaults.js b/src/plots/mapbox/layout_defaults.js index 7c381dd9144..b3584239b47 100644 --- a/src/plots/mapbox/layout_defaults.js +++ b/src/plots/mapbox/layout_defaults.js @@ -12,6 +12,7 @@ var Lib = require('../../lib'); var handleSubplotDefaults = require('../subplot_defaults'); +var handleArrayContainerDefaults = require('../array_container_defaults'); var layoutAttributes = require('./layout_attributes'); @@ -34,28 +35,22 @@ function handleDefaults(containerIn, containerOut, coerce, opts) { coerce('bearing'); coerce('pitch'); - handleLayerDefaults(containerIn, containerOut); + handleArrayContainerDefaults(containerIn, containerOut, { + name: 'layers', + handleItemDefaults: handleLayerDefaults + }); // copy ref to input container to update 'center' and 'zoom' on map move containerOut._input = containerIn; } -function handleLayerDefaults(containerIn, containerOut) { - var layersIn = containerIn.layers || [], - layersOut = containerOut.layers = []; - - var layerIn, layerOut; - +function handleLayerDefaults(layerIn, layerOut, mapboxOut, opts, itemOpts) { function coerce(attr, dflt) { return Lib.coerce(layerIn, layerOut, layoutAttributes.layers, attr, dflt); } - for(var i = 0; i < layersIn.length; i++) { - layerIn = layersIn[i]; - layerOut = {}; - - if(!Lib.isPlainObject(layerIn)) continue; - + var visible = coerce('visible', !itemOpts.itemIsNotPlainObject); + if(visible) { var sourceType = coerce('sourcetype'); coerce('source'); @@ -88,8 +83,5 @@ function handleLayerDefaults(containerIn, containerOut) { Lib.coerceFont(coerce, 'symbol.textfont'); coerce('symbol.textposition'); } - - layerOut._index = i; - layersOut.push(layerOut); } } diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index e987924bb33..b03b894cb98 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -2820,14 +2820,17 @@ describe('Test Axes.getTickformat', function() { it('get proper tickformatstop for linear axis', function() { var lineartickformatstops = [ { + visible: true, dtickrange: [null, 1], value: '.f2', }, { + visible: true, dtickrange: [1, 100], value: '.f1', }, { + visible: true, dtickrange: [100, null], value: 'g', } @@ -2854,6 +2857,19 @@ describe('Test Axes.getTickformat', function() { tickformatstops: lineartickformatstops, dtick: 99999 })).toEqual(lineartickformatstops[2].value); + + // a stop is ignored if it's set invisible, but the others are used + lineartickformatstops[1].visible = false; + expect(Axes.getTickFormat({ + type: 'linear', + tickformatstops: lineartickformatstops, + dtick: 99 + })).toBeUndefined(); + expect(Axes.getTickFormat({ + type: 'linear', + tickformatstops: lineartickformatstops, + dtick: 99999 + })).toEqual(lineartickformatstops[2].value); }); it('get proper tickformatstop for date axis', function() { @@ -2867,34 +2883,42 @@ describe('Test Axes.getTickformat', function() { var YEAR = 'M12'; // or 365.25 * DAY; var datetickformatstops = [ { + visible: true, dtickrange: [null, SECOND], value: '%H:%M:%S.%L ms' // millisecond }, { + visible: true, dtickrange: [SECOND, MINUTE], value: '%H:%M:%S s' // second }, { + visible: true, dtickrange: [MINUTE, HOUR], value: '%H:%M m' // minute }, { + visible: true, dtickrange: [HOUR, DAY], value: '%H:%M h' // hour }, { + visible: true, dtickrange: [DAY, WEEK], value: '%e. %b d' // day }, { + visible: true, dtickrange: [WEEK, MONTH], value: '%e. %b w' // week }, { + visible: true, dtickrange: [MONTH, YEAR], value: '%b \'%y M' // month }, { + visible: true, dtickrange: [YEAR, null], value: '%Y Y' // year } @@ -2945,18 +2969,22 @@ describe('Test Axes.getTickformat', function() { it('get proper tickformatstop for log axis', function() { var logtickformatstops = [ { + visible: true, dtickrange: [null, 'L0.01'], value: '.f3', }, { + visible: true, dtickrange: ['L0.01', 'L1'], value: '.f2', }, { + visible: true, dtickrange: ['D1', 'D2'], value: '.f1', }, { + visible: true, dtickrange: [1, null], value: 'g' } @@ -3090,7 +3118,7 @@ describe('Test tickformatstops:', function() { promise = promise.then(function() { return Plotly.relayout(gd, {'xaxis.tickformatstops': v}); }).then(function() { - expect(gd._fullLayout.xaxis.tickformatstops).toEqual([]); + expect(gd._fullLayout.xaxis.tickformatstops).toBeUndefined(); }); }); diff --git a/test/jasmine/tests/mapbox_test.js b/test/jasmine/tests/mapbox_test.js index ecfad33802c..f299da65c17 100644 --- a/test/jasmine/tests/mapbox_test.js +++ b/test/jasmine/tests/mapbox_test.js @@ -93,10 +93,17 @@ describe('mapbox defaults', function() { }; supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut.mapbox.layers[0].sourcetype).toEqual('geojson'); - expect(layoutOut.mapbox.layers[0]._index).toEqual(0); - expect(layoutOut.mapbox.layers[1].sourcetype).toEqual('geojson'); - expect(layoutOut.mapbox.layers[1]._index).toEqual(3); + expect(layoutOut.mapbox.layers).toEqual([jasmine.objectContaining({ + sourcetype: 'geojson', + _index: 0 + }), jasmine.objectContaining({ + visible: false + }), jasmine.objectContaining({ + visible: false + }), jasmine.objectContaining({ + sourcetype: 'geojson', + _index: 3 + })]); }); it('should coerce \'sourcelayer\' only for *vector* \'sourcetype\'', function() { @@ -566,7 +573,7 @@ describe('@noCI, mapbox plots', function() { } function getLayerLength(gd) { - return (gd.layout.mapbox.layers || []).length; + return Lib.filterVisible(gd._fullLayout.mapbox.layers || []).length; } function assertLayerStyle(gd, expectations, index) { @@ -605,6 +612,20 @@ describe('@noCI, mapbox plots', function() { expect(getLayerLength(gd)).toEqual(2); expect(countVisibleLayers(gd)).toEqual(2); + // hide a layer + return Plotly.relayout(gd, 'mapbox.layers[0].visible', false); + }) + .then(function() { + expect(getLayerLength(gd)).toEqual(1); + expect(countVisibleLayers(gd)).toEqual(1); + + // re-show it + return Plotly.relayout(gd, 'mapbox.layers[0].visible', true); + }) + .then(function() { + expect(getLayerLength(gd)).toEqual(2); + expect(countVisibleLayers(gd)).toEqual(2); + return Plotly.relayout(gd, mapUpdate); }) .then(function() { diff --git a/test/jasmine/tests/range_selector_test.js b/test/jasmine/tests/range_selector_test.js index 16c59a23466..5ff819d6218 100644 --- a/test/jasmine/tests/range_selector_test.js +++ b/test/jasmine/tests/range_selector_test.js @@ -38,10 +38,10 @@ describe('range selector defaults:', function() { supply(containerIn, containerOut); expect(containerOut.rangeselector) - .toEqual({ + .toEqual(jasmine.objectContaining({ visible: false, buttons: [] - }); + })); }); it('should coerce an empty button object', function() { @@ -56,6 +56,7 @@ describe('range selector defaults:', function() { expect(containerIn.rangeselector.buttons).toEqual([{}]); expect(containerOut.rangeselector.buttons).toEqual([{ + visible: true, step: 'month', stepmode: 'backward', count: 1, @@ -66,13 +67,13 @@ describe('range selector defaults:', function() { it('should skip over non-object buttons', function() { var containerIn = { rangeselector: { - buttons: [{ - label: 'button 0' - }, null, { - label: 'button 2' - }, 'remove', { - label: 'button 4' - }] + buttons: [ + {label: 'button 0'}, + null, + {label: 'button 2'}, + 'remove', + {label: 'button 4'} + ] } }; var containerOut = {}; @@ -80,7 +81,9 @@ describe('range selector defaults:', function() { supply(containerIn, containerOut); expect(containerIn.rangeselector.buttons.length).toEqual(5); - expect(containerOut.rangeselector.buttons.length).toEqual(3); + expect(containerOut.rangeselector.buttons.map(function(b) { + return b.visible; + })).toEqual([true, false, true, false, true]); }); it('should coerce all buttons present', function() { @@ -100,8 +103,8 @@ describe('range selector defaults:', function() { expect(containerOut.rangeselector.visible).toBe(true); expect(containerOut.rangeselector.buttons).toEqual([ - { step: 'year', stepmode: 'backward', count: 10, _index: 0 }, - { step: 'month', stepmode: 'backward', count: 6, _index: 1 } + { visible: true, step: 'year', stepmode: 'backward', count: 10, _index: 0 }, + { visible: true, step: 'month', stepmode: 'backward', count: 6, _index: 1 } ]); }); @@ -119,6 +122,7 @@ describe('range selector defaults:', function() { supply(containerIn, containerOut); expect(containerOut.rangeselector.buttons).toEqual([{ + visible: true, step: 'all', label: 'full range', _index: 0 @@ -490,9 +494,24 @@ describe('range selector interactions:', function() { }); } - it('should display the correct nodes', function() { + it('should display the correct nodes and can hide buttons', function(done) { + var allButtons = mockCopy.layout.xaxis.rangeselector.buttons.length; assertNodeCount('.rangeselector', 1); - assertNodeCount('.button', mockCopy.layout.xaxis.rangeselector.buttons.length); + assertNodeCount('.button', allButtons); + + Plotly.relayout(gd, 'xaxis.rangeselector.buttons[2].visible', false) + .then(function() { + assertNodeCount('.rangeselector', 1); + assertNodeCount('.button', allButtons - 1); + + return Plotly.relayout(gd, 'xaxis.rangeselector.buttons[2].visible', true); + }) + .then(function() { + assertNodeCount('.rangeselector', 1); + assertNodeCount('.button', allButtons); + }) + .catch(failTest) + .then(done); }); it('should be able to be removed by `relayout`', function(done) { diff --git a/test/jasmine/tests/sliders_test.js b/test/jasmine/tests/sliders_test.js index 84e165ce671..ff3851ca24d 100644 --- a/test/jasmine/tests/sliders_test.js +++ b/test/jasmine/tests/sliders_test.js @@ -98,25 +98,25 @@ describe('sliders defaults', function() { supply(layoutIn, layoutOut); expect(layoutOut.sliders[0].steps.length).toEqual(3); - expect(layoutOut.sliders[0].steps).toEqual([{ + expect(layoutOut.sliders[0].steps).toEqual([jasmine.objectContaining({ method: 'relayout', label: 'Label #1', value: 'label-1', execute: true, args: [] - }, { + }), jasmine.objectContaining({ method: 'update', label: 'Label #2', value: 'Label #2', execute: true, args: [] - }, { + }), jasmine.objectContaining({ method: 'animate', label: 'step-2', value: 'lacks-label', execute: true, args: [] - }]); + })]); }); it('should skip over non-object steps', function() { @@ -133,14 +133,17 @@ describe('sliders defaults', function() { supply(layoutIn, layoutOut); - expect(layoutOut.sliders[0].steps.length).toEqual(1); - expect(layoutOut.sliders[0].steps[0]).toEqual({ + expect(layoutOut.sliders[0].steps).toEqual([jasmine.objectContaining({ + visible: false + }), jasmine.objectContaining({ method: 'relayout', args: ['title', 'Hello World'], label: 'step-1', value: 'step-1', execute: true - }); + }), jasmine.objectContaining({ + visible: false + })]); }); it('should skip over steps with non-array \'args\' field', function() { @@ -158,14 +161,19 @@ describe('sliders defaults', function() { supply(layoutIn, layoutOut); - expect(layoutOut.sliders[0].steps.length).toEqual(1); - expect(layoutOut.sliders[0].steps[0]).toEqual({ + expect(layoutOut.sliders[0].steps).toEqual([jasmine.objectContaining({ + visible: false + }), jasmine.objectContaining({ method: 'relayout', args: ['title', 'Hello World'], label: 'step-1', value: 'step-1', execute: true - }); + }), jasmine.objectContaining({ + visible: false + }), jasmine.objectContaining({ + visible: false + })]); }); it('allows the `skip` method', function() { @@ -180,19 +188,18 @@ describe('sliders defaults', function() { supply(layoutIn, layoutOut); - expect(layoutOut.sliders[0].steps.length).toEqual(2); - expect(layoutOut.sliders[0].steps[0]).toEqual({ + expect(layoutOut.sliders[0].steps).toEqual([jasmine.objectContaining({ method: 'skip', label: 'step-0', value: 'step-0', execute: true, - }, { + }), jasmine.objectContaining({ method: 'skip', args: ['title', 'Hello World'], label: 'step-1', value: 'step-1', execute: true, - }); + })]); }); @@ -407,6 +414,58 @@ describe('sliders interactions', function() { .then(done); }); + it('only draws visible steps', function(done) { + function gripXFrac() { + var grip = document.querySelector('.' + constants.gripRectClass); + var transform = grip.attributes.transform.value; + var gripX = +(transform.split('(')[1].split(',')[0]); + var rail = document.querySelector('.' + constants.railRectClass); + var railWidth = +rail.attributes.width.value; + var railRX = +rail.attributes.rx.value; + return gripX / (railWidth - 2 * railRX); + } + function assertSlider(ticks, labels, gripx, active) { + assertNodeCount('.' + constants.groupClassName, 1); + assertNodeCount('.' + constants.tickRectClass, ticks); + assertNodeCount('.' + constants.labelGroupClass, labels); + expect(gripXFrac()).toBeWithin(gripx, 0.01); + expect(gd._fullLayout.sliders[1].active).toBe(active); + } + Plotly.relayout(gd, {'sliders[0].visible': false, 'sliders[1].active': 5}) + .then(function() { + assertSlider(15, 8, 5 / 14, 5); + + // hide two before the grip - grip moves left + return Plotly.relayout(gd, { + 'sliders[1].steps[0].visible': false, + 'sliders[1].steps[1].visible': false + }); + }) + .then(function() { + assertSlider(13, 7, 3 / 12, 5); + + // hide two after the grip - grip moves right, but not as far as + // the initial position since there are more steps to the right + return Plotly.relayout(gd, { + 'sliders[1].steps[12].visible': false, + 'sliders[1].steps[13].visible': false + }); + }) + .then(function() { + assertSlider(11, 6, 3 / 10, 5); + + // hide the active step - grip moves to 0, and first visible step is active + return Plotly.relayout(gd, { + 'sliders[1].steps[5].visible': false + }); + }) + .then(function() { + assertSlider(10, 5, 0, 2); + }) + .catch(failTest) + .then(done); + }); + it('should respond to mouse clicks', function(done) { var firstGroup = gd._fullLayout._infolayer.select('.' + constants.railTouchRectClass); var firstGrip = gd._fullLayout._infolayer.select('.' + constants.gripRectClass); diff --git a/test/jasmine/tests/updatemenus_test.js b/test/jasmine/tests/updatemenus_test.js index 9b7d64e0361..205eae923f1 100644 --- a/test/jasmine/tests/updatemenus_test.js +++ b/test/jasmine/tests/updatemenus_test.js @@ -9,8 +9,9 @@ var Drawing = require('@src/components/drawing'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var TRANSITION_DELAY = 100; -var fail = require('../assets/fail_test'); +var failTest = require('../assets/fail_test'); var getBBox = require('../assets/get_bbox'); +var delay = require('../assets/delay'); describe('update menus defaults', function() { 'use strict'; @@ -45,14 +46,15 @@ describe('update menus defaults', function() { supply(layoutIn, layoutOut); expect(layoutIn.updatemenus).toEqual(updatemenus); + expect(layoutOut.updatemenus.length).toEqual(layoutIn.updatemenus.length); layoutOut.updatemenus.forEach(function(item, i) { - expect(item).toEqual({ + expect(item).toEqual(jasmine.objectContaining({ visible: false, buttons: [], _input: {}, _index: i - }); + })); }); }); @@ -93,7 +95,7 @@ describe('update menus defaults', function() { expect(layoutOut.updatemenus[2].active).toBeUndefined(); }); - it('should skip over non-object buttons', function() { + it('should set non-object buttons visible: false', function() { layoutIn.updatemenus = [{ buttons: [ null, @@ -107,17 +109,23 @@ describe('update menus defaults', function() { supply(layoutIn, layoutOut); - expect(layoutOut.updatemenus[0].buttons.length).toEqual(1); - expect(layoutOut.updatemenus[0].buttons[0]).toEqual({ - method: 'relayout', - args: ['title', 'Hello World'], - execute: true, - label: '', - _index: 1 + expect(layoutOut.updatemenus[0].buttons.length).toEqual(3); + [0, 2].forEach(function(i) { + expect(layoutOut.updatemenus[0].buttons[i]).toEqual( + jasmine.objectContaining({visible: false})); }); + expect(layoutOut.updatemenus[0].buttons[1]).toEqual( + jasmine.objectContaining({ + visible: true, + method: 'relayout', + args: ['title', 'Hello World'], + execute: true, + label: '', + _index: 1 + })); }); - it('should skip over buttons with array \'args\' field', function() { + it('should skip over buttons without array \'args\' field', function() { layoutIn.updatemenus = [{ buttons: [{ method: 'restyle', @@ -132,17 +140,23 @@ describe('update menus defaults', function() { supply(layoutIn, layoutOut); - expect(layoutOut.updatemenus[0].buttons.length).toEqual(1); - expect(layoutOut.updatemenus[0].buttons[0]).toEqual({ - method: 'relayout', - args: ['title', 'Hello World'], - execute: true, - label: '', - _index: 1 + expect(layoutOut.updatemenus[0].buttons.length).toEqual(4); + [0, 2, 3].forEach(function(i) { + expect(layoutOut.updatemenus[0].buttons[i]).toEqual( + jasmine.objectContaining({visible: false})); }); + expect(layoutOut.updatemenus[0].buttons[1]).toEqual( + jasmine.objectContaining({ + visible: true, + method: 'relayout', + args: ['title', 'Hello World'], + execute: true, + label: '', + _index: 1 + })); }); - it('allow the `skip` method', function() { + it('allows the `skip` method with no args', function() { layoutIn.updatemenus = [{ buttons: [{ method: 'skip', @@ -155,18 +169,21 @@ describe('update menus defaults', function() { supply(layoutIn, layoutOut); expect(layoutOut.updatemenus[0].buttons.length).toEqual(2); - expect(layoutOut.updatemenus[0].buttons[0]).toEqual({ + expect(layoutOut.updatemenus[0].buttons[0]).toEqual(jasmine.objectContaining({ + visible: true, method: 'skip', label: '', execute: true, _index: 0 - }, { + })); + expect(layoutOut.updatemenus[0].buttons[1]).toEqual(jasmine.objectContaining({ + visible: true, method: 'skip', args: ['title', 'Hello World'], label: '', execute: true, _index: 1 - }); + })); }); it('should keep ref to input update menu container', function() { @@ -264,7 +281,9 @@ describe('update menus buttons', function() { buttonMenus = allMenus.filter(function(opts) { return opts.type === 'buttons'; }); dropdownMenus = allMenus.filter(function(opts) { return opts.type !== 'buttons'; }); - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + Plotly.plot(gd, mockCopy.data, mockCopy.layout) + .catch(failTest) + .then(done); }); afterEach(function() { @@ -272,7 +291,7 @@ describe('update menus buttons', function() { destroyGraphDiv(); }); - it('creates button menus', function(done) { + it('creates button menus', function() { assertNodeCount('.' + constants.containerClassName, 1); // 12 menus, but button menus don't have headers, so there are only six headers: @@ -283,8 +302,6 @@ describe('update menus buttons', function() { buttonMenus.forEach(function(menu) { buttonCount += menu.buttons.length; }); assertNodeCount('.' + constants.buttonClassName, buttonCount); - - done(); }); function assertNodeCount(query, cnt) { @@ -306,7 +323,9 @@ describe('update menus initialization', function() { {method: 'restyle', args: [], label: 'second'}, ] }] - }).then(done); + }) + .catch(failTest) + .then(done); }); afterEach(function() { @@ -335,7 +354,9 @@ describe('update menus interactions', function() { var mockCopy = Lib.extendDeep({}, mock); mockCopy.layout.updatemenus[1].x = 1; - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + Plotly.plot(gd, mockCopy.data, mockCopy.layout) + .catch(failTest) + .then(done); }); afterEach(function() { @@ -419,6 +440,7 @@ describe('update menus interactions', function() { assertPushMargins([false, false, false]); }) + .catch(failTest) .then(done); }); @@ -446,8 +468,29 @@ describe('update menus interactions', function() { return click(header0); }).then(function() { assertMenus([3, 0]); - done(); - }); + }) + .catch(failTest) + .then(done); + }); + + it('can set buttons visible or hidden', function(done) { + assertMenus([0, 0]); + click(selectHeader(1)) + .then(function() { + assertMenus([0, 4]); + return Plotly.relayout(gd, {'updatemenus[1].buttons[1].visible': false}); + }) + .then(delay(4 * TRANSITION_DELAY)) + .then(function() { + assertMenus([0, 3]); + return Plotly.relayout(gd, {'updatemenus[1].buttons[1].visible': true}); + }) + .then(delay(4 * TRANSITION_DELAY)) + .then(function() { + assertMenus([0, 4]); + }) + .catch(failTest) + .then(done); }); it('should execute the API command when execute = true', function(done) { @@ -458,7 +501,9 @@ describe('update menus interactions', function() { }).then(function() { // Has been changed: expect(gd.data[0].line.color).toEqual('green'); - }).catch(fail).then(done); + }) + .catch(failTest) + .then(done); }); it('should not execute the API command when execute = false', function(done) { @@ -473,7 +518,9 @@ describe('update menus interactions', function() { }).then(function() { // Is unchanged: expect(gd.data[0].line.color).toEqual('blue'); - }).catch(fail).then(done); + }) + .catch(failTest) + .then(done); }); it('should emit an event on button click', function(done) { @@ -498,7 +545,9 @@ describe('update menus interactions', function() { expect(clickCnt).toEqual(2); expect(data.length).toEqual(2); expect(data[1].active).toEqual(1); - }).catch(fail).then(done); + }) + .catch(failTest) + .then(done); }); it('should still emit the event if method = skip', function(done) { @@ -518,14 +567,16 @@ describe('update menus interactions', function() { 'updatemenus[1].buttons[2].method': 'skip', 'updatemenus[1].buttons[3].method': 'skip', }).then(function() { - click(selectHeader(0)).then(function() { - expect(clickCnt).toEqual(0); + return click(selectHeader(0)); + }).then(function() { + expect(clickCnt).toEqual(0); - return click(selectButton(2)); - }).then(function() { - expect(clickCnt).toEqual(1); - }).catch(fail).then(done); - }); + return click(selectButton(2)); + }).then(function() { + expect(clickCnt).toEqual(1); + }) + .catch(failTest) + .then(done); }); it('should apply update on button click', function(done) { @@ -548,9 +599,9 @@ describe('update menus interactions', function() { return click(selectButton(0)); }).then(function() { assertActive(gd, [0, 0]); - - done(); - }); + }) + .catch(failTest) + .then(done); }); it('should update correctly on failed binding comparisons', function(done) { @@ -596,6 +647,7 @@ describe('update menus interactions', function() { .then(function() { assertActive(gd, [1]); }) + .catch(failTest) .then(done); }); @@ -629,9 +681,9 @@ describe('update menus interactions', function() { assertItemColor(button, activeColor); mouseEvent('mouseout', button); assertItemColor(button, activeColor); - - done(); - }); + }) + .catch(failTest) + .then(done); }); it('should relayout', function(done) { @@ -684,9 +736,9 @@ describe('update menus interactions', function() { }).then(function() { assertItemColor(selectHeader(0), 'rgb(0, 0, 0)'); assertItemColor(selectHeader(1), 'rgb(0, 0, 0)'); - - done(); - }); + }) + .catch(failTest) + .then(done); }); it('applies padding on all sides', function(done) { @@ -717,7 +769,9 @@ describe('update menus interactions', function() { expect(xy1[0] - xy2[0]).toEqual(xpad); expect(xy1[1] - xy2[1]).toEqual(ypad); - }).catch(fail).then(done); + }) + .catch(failTest) + .then(done); }); it('applies y padding on relayout', function(done) { @@ -740,7 +794,9 @@ describe('update menus interactions', function() { x2 = parseInt(firstMenu.attr('transform').match(/translate\(([^,]*).*/)[1]); expect(x1 - x2).toBeCloseTo(padShift, 1); - }).catch(fail).then(done); + }) + .catch(failTest) + .then(done); }); function assertNodeCount(query, cnt) { @@ -880,7 +936,7 @@ describe('update menus interaction with other components:', function() { expect(menuLayer.selectAll('.updatemenu-container').size()).toBe(1); expect(infoLayer.node().nextSibling).toBe(menuLayer.node()); }) - .catch(fail) + .catch(failTest) .then(done); }); }); @@ -990,7 +1046,9 @@ describe('update menus interaction with scrollbox:', function() { menuLeft = menus[2]; menuRight = menus[3]; menuUp = menus[4]; - }).catch(fail).then(done); + }) + .catch(failTest) + .then(done); }); afterEach(function() { From 3dee25c4e1e1f35bfaab06554bec434a23f81348 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 20 Jun 2018 21:38:39 -0400 Subject: [PATCH 06/24] use handleArrayContainerDefaults for various array containers: rangeselector.buttons updatemenus.buttons parcoords & splom.dimensions (and DRY these a bit) --- src/components/rangeselector/defaults.js | 46 +++++------ src/components/updatemenus/defaults.js | 36 +++------ src/traces/parcoords/defaults.js | 93 +++++++++-------------- src/traces/parcoords/merge_length.js | 36 +++++++++ src/traces/splom/defaults.js | 54 ++++--------- test/jasmine/tests/parcoords_test.js | 16 ++-- test/jasmine/tests/range_selector_test.js | 12 +-- 7 files changed, 134 insertions(+), 159 deletions(-) create mode 100644 src/traces/parcoords/merge_length.js diff --git a/src/components/rangeselector/defaults.js b/src/components/rangeselector/defaults.js index 1e3624f2870..0110ec05ae6 100644 --- a/src/components/rangeselector/defaults.js +++ b/src/components/rangeselector/defaults.js @@ -11,6 +11,7 @@ var Lib = require('../../lib'); var Color = require('../color'); var Template = require('../../plot_api/plot_template'); +var handleArrayContainerDefaults = require('../../plots/array_container_defaults'); var attributes = require('./attributes'); var buttonAttrs = require('./button_attributes'); @@ -25,7 +26,11 @@ module.exports = function handleDefaults(containerIn, containerOut, layout, coun return Lib.coerce(selectorIn, selectorOut, attributes, attr, dflt); } - var buttons = buttonsDefaults(selectorIn, selectorOut, calendar); + var buttons = handleArrayContainerDefaults(selectorIn, selectorOut, { + name: 'buttons', + handleItemDefaults: buttonDefaults, + calendar: calendar + }); var visible = coerce('visible', buttons.length > 0); if(!visible) return; @@ -46,43 +51,30 @@ module.exports = function handleDefaults(containerIn, containerOut, layout, coun coerce('borderwidth'); }; -function buttonsDefaults(containerIn, containerOut, calendar) { - var buttonsIn = containerIn.buttons || [], - buttonsOut = containerOut.buttons = []; - - var buttonIn, buttonOut; +function buttonDefaults(buttonIn, buttonOut, selectorOut, opts, itemOpts) { + var calendar = opts.calendar; function coerce(attr, dflt) { return Lib.coerce(buttonIn, buttonOut, buttonAttrs, attr, dflt); } - for(var i = 0; i < buttonsIn.length; i++) { - buttonIn = buttonsIn[i]; - buttonOut = {}; - - var visible = coerce('visible', Lib.isPlainObject(buttonIn)); + var visible = coerce('visible', !itemOpts.itemIsNotPlainObject); - if(visible) { - var step = coerce('step'); - if(step !== 'all') { - if(calendar && calendar !== 'gregorian' && (step === 'month' || step === 'year')) { - buttonOut.stepmode = 'backward'; - } - else { - coerce('stepmode'); - } - - coerce('count'); + if(visible) { + var step = coerce('step'); + if(step !== 'all') { + if(calendar && calendar !== 'gregorian' && (step === 'month' || step === 'year')) { + buttonOut.stepmode = 'backward'; + } + else { + coerce('stepmode'); } - coerce('label'); + coerce('count'); } - buttonOut._index = i; - buttonsOut.push(buttonOut); + coerce('label'); } - - return buttonsOut; } function getPosDflt(containerOut, layout, counterAxes) { diff --git a/src/components/updatemenus/defaults.js b/src/components/updatemenus/defaults.js index 38dedd0d1c3..96fd5e0dc54 100644 --- a/src/components/updatemenus/defaults.js +++ b/src/components/updatemenus/defaults.js @@ -33,7 +33,10 @@ function menuDefaults(menuIn, menuOut, layoutOut) { return Lib.coerce(menuIn, menuOut, attributes, attr, dflt); } - var buttons = buttonsDefaults(menuIn, menuOut); + var buttons = handleArrayContainerDefaults(menuIn, menuOut, { + name: 'buttons', + handleItemDefaults: buttonDefaults + }); var visible = coerce('visible', buttons.length > 0); if(!visible) return; @@ -62,32 +65,17 @@ function menuDefaults(menuIn, menuOut, layoutOut) { coerce('borderwidth'); } -function buttonsDefaults(menuIn, menuOut) { - var buttonsIn = menuIn.buttons || [], - buttonsOut = menuOut.buttons = []; - - var buttonIn, buttonOut; - +function buttonDefaults(buttonIn, buttonOut, selectorOut, opts, itemOpts) { function coerce(attr, dflt) { return Lib.coerce(buttonIn, buttonOut, buttonAttrs, attr, dflt); } - for(var i = 0; i < buttonsIn.length; i++) { - buttonIn = buttonsIn[i]; - buttonOut = {}; - - var visible = coerce('visible', Lib.isPlainObject(buttonIn) && - (buttonIn.method === 'skip' || Array.isArray(buttonIn.args))); - if(visible) { - coerce('method'); - coerce('args'); - coerce('label'); - coerce('execute'); - } - - buttonOut._index = i; - buttonsOut.push(buttonOut); + var visible = coerce('visible', !itemOpts.itemIsNotPlainObject && + (buttonIn.method === 'skip' || Array.isArray(buttonIn.args))); + if(visible) { + coerce('method'); + coerce('args'); + coerce('label'); + coerce('execute'); } - - return buttonsOut; } diff --git a/src/traces/parcoords/defaults.js b/src/traces/parcoords/defaults.js index 07d1e97b0a8..3ce8cf0a168 100644 --- a/src/traces/parcoords/defaults.js +++ b/src/traces/parcoords/defaults.js @@ -9,12 +9,15 @@ 'use strict'; var Lib = require('../../lib'); -var attributes = require('./attributes'); var hasColorscale = require('../../components/colorscale/has_colorscale'); var colorscaleDefaults = require('../../components/colorscale/defaults'); -var maxDimensionCount = require('./constants').maxDimensionCount; var handleDomainDefaults = require('../../plots/domain').defaults; +var handleArrayContainerDefaults = require('../../plots/array_container_defaults'); + +var attributes = require('./attributes'); var axisBrush = require('./axisbrush'); +var maxDimensionCount = require('./constants').maxDimensionCount; +var mergeLength = require('./merge_length'); function handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce) { var lineColor = coerce('line.color', defaultColor); @@ -26,67 +29,39 @@ function handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce) { // TODO: I think it would be better to keep showing lines beyond the last line color // but I'm not sure what color to give these lines - probably black or white // depending on the background color? - traceOut._length = Math.min(traceOut._length, lineColor.length); + return lineColor.length; } else { traceOut.line.color = defaultColor; } } + return Infinity; } -function dimensionsDefaults(traceIn, traceOut) { - var dimensionsIn = traceIn.dimensions || [], - dimensionsOut = traceOut.dimensions = []; - - var dimensionIn, dimensionOut, i; - var commonLength = Infinity; - - if(dimensionsIn.length > maxDimensionCount) { - Lib.log('parcoords traces support up to ' + maxDimensionCount + ' dimensions at the moment'); - dimensionsIn.splice(maxDimensionCount); - } - +function dimensionDefaults(dimensionIn, dimensionOut, traceOut, opts, itemOpts) { function coerce(attr, dflt) { return Lib.coerce(dimensionIn, dimensionOut, attributes.dimensions, attr, dflt); } - for(i = 0; i < dimensionsIn.length; i++) { - dimensionIn = dimensionsIn[i]; - dimensionOut = {}; - - if(!Lib.isPlainObject(dimensionIn)) { - continue; - } - - var values = coerce('values'); - var visible = coerce('visible'); - if(!(values && values.length)) { - visible = dimensionOut.visible = false; - } - - if(visible) { - coerce('label'); - coerce('tickvals'); - coerce('ticktext'); - coerce('tickformat'); - coerce('range'); - - coerce('multiselect'); - var constraintRange = coerce('constraintrange'); - if(constraintRange) { - dimensionOut.constraintrange = axisBrush.cleanRanges(constraintRange, dimensionOut); - } + var values = coerce('values'); + var visible = coerce('visible'); + if(!(values && values.length && !itemOpts.itemIsNotPlainObject)) { + visible = dimensionOut.visible = false; + } - commonLength = Math.min(commonLength, values.length); + if(visible) { + coerce('label'); + coerce('tickvals'); + coerce('ticktext'); + coerce('tickformat'); + coerce('range'); + + coerce('multiselect'); + var constraintRange = coerce('constraintrange'); + if(constraintRange) { + dimensionOut.constraintrange = axisBrush.cleanRanges(constraintRange, dimensionOut); } - - dimensionOut._index = i; - dimensionsOut.push(dimensionOut); } - - traceOut._length = commonLength; - - return dimensionsOut; } module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { @@ -94,9 +69,18 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } - var dimensions = dimensionsDefaults(traceIn, traceOut); + var dimensionsIn = traceIn.dimensions; + if(Array.isArray(dimensionsIn) && dimensionsIn.length > maxDimensionCount) { + Lib.log('parcoords traces support up to ' + maxDimensionCount + ' dimensions at the moment'); + dimensionsIn.splice(maxDimensionCount); + } + + var dimensions = handleArrayContainerDefaults(traceIn, traceOut, { + name: 'dimensions', + handleItemDefaults: dimensionDefaults + }); - handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce); + var len = handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce); handleDomainDefaults(traceOut, layout, coerce); @@ -104,12 +88,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout traceOut.visible = false; } - // since we're not slicing uneven arrays anymore, stash the length in each dimension - // but we can't do this in dimensionsDefaults (yet?) because line.color can also - // truncate - for(var i = 0; i < dimensions.length; i++) { - if(dimensions[i].visible) dimensions[i]._length = traceOut._length; - } + mergeLength(traceOut, dimensions, 'values', len); // make default font size 10px (default is 12), // scale linearly with global font size diff --git a/src/traces/parcoords/merge_length.js b/src/traces/parcoords/merge_length.js new file mode 100644 index 00000000000..eccb5326c7b --- /dev/null +++ b/src/traces/parcoords/merge_length.js @@ -0,0 +1,36 @@ +/** +* Copyright 2012-2018, 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'; + +/** + * mergeLength: set trace length as the minimum of all dimension data lengths + * and propagates this length into each dimension + * + * @param {object} traceOut: the fullData trace + * @param {Array(object)} dimensions: array of dimension objects + * @param {string} dataAttr: the attribute of each dimension containing the data + * @param {integer} len: an already-existing length from other attributes + */ +module.exports = function(traceOut, dimensions, dataAttr, len) { + if(!len) len = Infinity; + var i, dimi; + for(i = 0; i < dimensions.length; i++) { + dimi = dimensions[i]; + if(dimi.visible) len = Math.min(len, dimi[dataAttr].length); + } + if(len === Infinity) len = 0; + + traceOut._length = len; + for(i = 0; i < dimensions.length; i++) { + dimi = dimensions[i]; + if(dimi.visible) dimi._length = len; + } + + return len; +}; diff --git a/src/traces/splom/defaults.js b/src/traces/splom/defaults.js index 8e5f930d085..72665747321 100644 --- a/src/traces/splom/defaults.js +++ b/src/traces/splom/defaults.js @@ -9,10 +9,12 @@ 'use strict'; var Lib = require('../../lib'); +var handleArrayContainerDefaults = require('../../plots/array_container_defaults'); var attributes = require('./attributes'); var subTypes = require('../scatter/subtypes'); var handleMarkerDefaults = require('../scatter/marker_defaults'); +var mergeLength = require('../parcoords/merge_length'); var OPEN_RE = /-open/; module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { @@ -20,12 +22,17 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } - var dimLength = handleDimensionsDefaults(traceIn, traceOut); + var dimensions = handleArrayContainerDefaults(traceIn, traceOut, { + name: 'dimensions', + handleItemDefaults: dimensionDefaults + }); var showDiag = coerce('diagonal.visible'); var showUpper = coerce('showupperhalf'); var showLower = coerce('showlowerhalf'); + var dimLength = mergeLength(traceOut, dimensions, 'values'); + if(!dimLength || (!showDiag && !showUpper && !showLower)) { traceOut.visible = false; return; @@ -44,51 +51,18 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout Lib.coerceSelectionMarkerOpacity(traceOut, coerce); }; -function handleDimensionsDefaults(traceIn, traceOut) { - var dimensionsIn = traceIn.dimensions; - if(!Array.isArray(dimensionsIn)) return 0; - - var dimLength = dimensionsIn.length; - var commonLength = 0; - var dimensionsOut = traceOut.dimensions = new Array(dimLength); - var dimIn; - var dimOut; - var i; - +function dimensionDefaults(dimIn, dimOut, traceOut, opts, itemOpts) { function coerce(attr, dflt) { return Lib.coerce(dimIn, dimOut, attributes.dimensions, attr, dflt); } - for(i = 0; i < dimLength; i++) { - dimIn = dimensionsIn[i]; - dimOut = dimensionsOut[i] = {}; + coerce('label'); + coerce('visible'); + var values = coerce('values'); - // coerce label even if dimensions may be `visible: false`, - // to fill in axis title defaults - coerce('label'); - - // wait until plot step to filter out visible false dimensions - var visible = coerce('visible'); - if(!visible) continue; - - var values = coerce('values'); - if(!values || !values.length) { - dimOut.visible = false; - continue; - } - - commonLength = Math.max(commonLength, values.length); - dimOut._index = i; - } - - for(i = 0; i < dimLength; i++) { - dimOut = dimensionsOut[i]; - if(dimOut.visible) dimOut._length = commonLength; + if(!(values && values.length && !itemOpts.itemIsNotPlainObject)) { + dimOut.visible = false; } - - traceOut._length = commonLength; - - return dimensionsOut.length; } function handleAxisDefaults(traceIn, traceOut, layout, coerce) { diff --git a/test/jasmine/tests/parcoords_test.js b/test/jasmine/tests/parcoords_test.js index a4ea3e41e86..0f696eaff5a 100644 --- a/test/jasmine/tests/parcoords_test.js +++ b/test/jasmine/tests/parcoords_test.js @@ -136,14 +136,14 @@ describe('parcoords initialization tests', function() { alienProperty: 'Alpha Centauri' }] }); - expect(fullTrace.dimensions).toEqual([{ + expect(fullTrace.dimensions).toEqual([jasmine.objectContaining({ values: [1], visible: true, tickformat: '3s', multiselect: true, _index: 0, _length: 1 - }]); + })]); }); it('\'dimension.visible\' should be set to false, and other props just passed through if \'values\' is not provided', function() { @@ -152,7 +152,9 @@ describe('parcoords initialization tests', function() { alienProperty: 'Alpha Centauri' }] }); - expect(fullTrace.dimensions).toEqual([{visible: false, _index: 0}]); + expect(fullTrace.dimensions).toEqual([jasmine.objectContaining({ + visible: false, _index: 0 + })]); }); it('\'dimension.visible\' should be set to false, and other props just passed through if \'values\' is an empty array', function() { @@ -162,7 +164,9 @@ describe('parcoords initialization tests', function() { alienProperty: 'Alpha Centauri' }] }); - expect(fullTrace.dimensions).toEqual([{visible: false, values: [], _index: 0}]); + expect(fullTrace.dimensions).toEqual([jasmine.objectContaining({ + visible: false, values: [], _index: 0 + })]); }); it('\'dimension.visible\' should be set to false, and other props just passed through if \'values\' is not an array', function() { @@ -172,7 +176,9 @@ describe('parcoords initialization tests', function() { alienProperty: 'Alpha Centauri' }] }); - expect(fullTrace.dimensions).toEqual([{visible: false, _index: 0}]); + expect(fullTrace.dimensions).toEqual([jasmine.objectContaining({ + visible: false, _index: 0 + })]); }); it('\'dimension.values\' should get truncated to a common shortest *nonzero* length', function() { diff --git a/test/jasmine/tests/range_selector_test.js b/test/jasmine/tests/range_selector_test.js index 5ff819d6218..c63e17039cf 100644 --- a/test/jasmine/tests/range_selector_test.js +++ b/test/jasmine/tests/range_selector_test.js @@ -55,13 +55,13 @@ describe('range selector defaults:', function() { supply(containerIn, containerOut); expect(containerIn.rangeselector.buttons).toEqual([{}]); - expect(containerOut.rangeselector.buttons).toEqual([{ + expect(containerOut.rangeselector.buttons).toEqual([jasmine.objectContaining({ visible: true, step: 'month', stepmode: 'backward', count: 1, _index: 0 - }]); + })]); }); it('should skip over non-object buttons', function() { @@ -103,8 +103,8 @@ describe('range selector defaults:', function() { expect(containerOut.rangeselector.visible).toBe(true); expect(containerOut.rangeselector.buttons).toEqual([ - { visible: true, step: 'year', stepmode: 'backward', count: 10, _index: 0 }, - { visible: true, step: 'month', stepmode: 'backward', count: 6, _index: 1 } + jasmine.objectContaining({ visible: true, step: 'year', stepmode: 'backward', count: 10, _index: 0 }), + jasmine.objectContaining({ visible: true, step: 'month', stepmode: 'backward', count: 6, _index: 1 }) ]); }); @@ -121,12 +121,12 @@ describe('range selector defaults:', function() { supply(containerIn, containerOut); - expect(containerOut.rangeselector.buttons).toEqual([{ + expect(containerOut.rangeselector.buttons).toEqual([jasmine.objectContaining({ visible: true, step: 'all', label: 'full range', _index: 0 - }]); + })]); }); it('should use axis and counter axis to determine \'x\' and \'y\' defaults (case 1 y)', function() { From 5cca8d399c10cc3a4ad79136471c4202653a69e1 Mon Sep 17 00:00:00 2001 From: bpostlethwaite Date: Thu, 12 Apr 2018 11:11:05 -0400 Subject: [PATCH 07/24] templates - big commit implementing most of it, coerce-level integration --- src/components/colorbar/defaults.js | 4 +- src/components/errorbars/defaults.js | 7 +- src/components/grid/index.js | 10 +- src/components/legend/defaults.js | 12 +- src/components/rangeslider/defaults.js | 17 +- src/lib/coerce.js | 37 +++- src/plot_api/index.js | 1 + src/plot_api/make_template.js | 210 ++++++++++++++++++++ src/plot_api/plot_template.js | 183 +++++++++++++++++ src/plot_api/validate.js | 13 +- src/plots/array_container_defaults.js | 28 ++- src/plots/cartesian/layout_defaults.js | 5 +- src/plots/gl3d/layout/axis_defaults.js | 8 +- src/plots/layout_attributes.js | 23 +++ src/plots/plots.js | 34 +++- src/plots/polar/layout_defaults.js | 4 + src/plots/subplot_defaults.js | 5 +- src/plots/ternary/layout/defaults.js | 4 +- test/jasmine/tests/annotations_test.js | 4 +- test/jasmine/tests/heatmap_test.js | 4 +- test/jasmine/tests/layout_images_test.js | 6 +- test/jasmine/tests/plots_test.js | 10 +- test/jasmine/tests/range_slider_test.js | 28 +-- test/jasmine/tests/shapes_test.js | 4 +- test/jasmine/tests/template_test.js | 41 ++++ test/jasmine/tests/transform_filter_test.js | 6 +- test/jasmine/tests/transform_multi_test.js | 10 +- test/jasmine/tests/transform_sort_test.js | 2 +- test/jasmine/tests/validate_test.js | 17 +- 29 files changed, 637 insertions(+), 100 deletions(-) create mode 100644 src/plot_api/make_template.js create mode 100644 src/plot_api/plot_template.js create mode 100644 test/jasmine/tests/template_test.js diff --git a/src/components/colorbar/defaults.js b/src/components/colorbar/defaults.js index 0eaca643ca0..6ad1e3d5cbb 100644 --- a/src/components/colorbar/defaults.js +++ b/src/components/colorbar/defaults.js @@ -10,6 +10,8 @@ 'use strict'; var Lib = require('../../lib'); +var Template = require('../../plot_api/plot_template'); + var handleTickValueDefaults = require('../../plots/cartesian/tick_value_defaults'); var handleTickMarkDefaults = require('../../plots/cartesian/tick_mark_defaults'); var handleTickLabelDefaults = require('../../plots/cartesian/tick_label_defaults'); @@ -18,7 +20,7 @@ var attributes = require('./attributes'); module.exports = function colorbarDefaults(containerIn, containerOut, layout) { - var colorbarOut = containerOut.colorbar = {}, + var colorbarOut = Template.newContainer(containerOut, 'colorbar'), colorbarIn = containerIn.colorbar || {}; function coerce(attr, dflt) { diff --git a/src/components/errorbars/defaults.js b/src/components/errorbars/defaults.js index 0eb5a2f75b5..056e40909ef 100644 --- a/src/components/errorbars/defaults.js +++ b/src/components/errorbars/defaults.js @@ -12,14 +12,15 @@ var isNumeric = require('fast-isnumeric'); var Registry = require('../../registry'); var Lib = require('../../lib'); +var Template = require('../../plot_api/plot_template'); var attributes = require('./attributes'); module.exports = function(traceIn, traceOut, defaultColor, opts) { - var objName = 'error_' + opts.axis, - containerOut = traceOut[objName] = {}, - containerIn = traceIn[objName] || {}; + var objName = 'error_' + opts.axis; + var containerOut = Template.newContainer(traceOut, objName); + var containerIn = traceIn[objName] || {}; function coerce(attr, dflt) { return Lib.coerce(containerIn, containerOut, attributes, attr, dflt); diff --git a/src/components/grid/index.js b/src/components/grid/index.js index 357700670f6..ba1a451b6b3 100644 --- a/src/components/grid/index.js +++ b/src/components/grid/index.js @@ -12,6 +12,7 @@ var Lib = require('../../lib'); var counterRegex = require('../../lib/regex').counter; var domainAttrs = require('../../plots/domain').attributes; var cartesianIdRegex = require('../../plots/cartesian/constants').idRegex; +var Template = require('../../plot_api/plot_template'); var gridAttrs = { rows: { @@ -201,7 +202,7 @@ function sizeDefaults(layoutIn, layoutOut) { if(hasXaxes) dfltColumns = xAxes.length; } - var gridOut = {}; + var gridOut = Template.newContainer(layoutOut, 'grid'); function coerce(attr, dflt) { return Lib.coerce(gridIn, gridOut, gridAttrs, attr, dflt); @@ -210,7 +211,10 @@ function sizeDefaults(layoutIn, layoutOut) { var rows = coerce('rows', dfltRows); var columns = coerce('columns', dfltColumns); - if(!(rows * columns > 1)) return; + if(!(rows * columns > 1)) { + delete layoutOut.grid; + return; + } if(!hasSubplotGrid && !hasXaxes && !hasYaxes) { var useDefaultSubplots = coerce('pattern') === 'independent'; @@ -234,8 +238,6 @@ function sizeDefaults(layoutIn, layoutOut) { x: fillGridPositions('x', coerce, dfltGapX, dfltSideX, columns), y: fillGridPositions('y', coerce, dfltGapY, dfltSideY, rows, reversed) }; - - layoutOut.grid = gridOut; } // coerce x or y sizing attributes and return an array of domains for this direction diff --git a/src/components/legend/defaults.js b/src/components/legend/defaults.js index 8dbf3cf8e9c..a27d0c9c6a9 100644 --- a/src/components/legend/defaults.js +++ b/src/components/legend/defaults.js @@ -11,6 +11,7 @@ var Registry = require('../../registry'); var Lib = require('../../lib'); +var Template = require('../../plot_api/plot_template'); var attributes = require('./attributes'); var basePlotLayoutAttributes = require('../../plots/layout_attributes'); @@ -19,7 +20,6 @@ var helpers = require('./helpers'); module.exports = function legendDefaults(layoutIn, layoutOut, fullData) { var containerIn = layoutIn.legend || {}; - var containerOut = {}; var visibleTraces = 0; var defaultOrder = 'normal'; @@ -47,16 +47,16 @@ module.exports = function legendDefaults(layoutIn, layoutOut, fullData) { } } - function coerce(attr, dflt) { - return Lib.coerce(containerIn, containerOut, attributes, attr, dflt); - } - var showLegend = Lib.coerce(layoutIn, layoutOut, basePlotLayoutAttributes, 'showlegend', visibleTraces > 1); if(showLegend === false) return; - layoutOut.legend = containerOut; + var containerOut = Template.newContainer(layoutOut, 'legend'); + + function coerce(attr, dflt) { + return Lib.coerce(containerIn, containerOut, attributes, attr, dflt); + } coerce('bgcolor', layoutOut.paper_bgcolor); coerce('bordercolor'); diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js index 651bfc72e7f..b262b762a26 100644 --- a/src/components/rangeslider/defaults.js +++ b/src/components/rangeslider/defaults.js @@ -9,9 +9,11 @@ 'use strict'; var Lib = require('../../lib'); +var Template = require('../../plot_api/plot_template'); +var axisIds = require('../../plots/cartesian/axis_ids'); + var attributes = require('./attributes'); var oppAxisAttrs = require('./oppaxis_attributes'); -var axisIds = require('../../plots/cartesian/axis_ids'); module.exports = function handleDefaults(layoutIn, layoutOut, axName) { var axIn = layoutIn[axName]; @@ -25,13 +27,14 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) { } var containerIn = axIn.rangeslider; - var containerOut = axOut.rangeslider = {}; + var containerOut = Template.newContainer(axOut, 'rangeslider'); function coerce(attr, dflt) { return Lib.coerce(containerIn, containerOut, attributes, attr, dflt); } - function coerceRange(rangeContainerIn, rangeContainerOut, attr, dflt) { + var rangeContainerIn, rangeContainerOut; + function coerceRange(attr, dflt) { return Lib.coerce(rangeContainerIn, rangeContainerOut, oppAxisAttrs, attr, dflt); } @@ -59,8 +62,8 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) { for(var i = 0; i < yNames.length; i++) { var yName = yNames[i]; - var rangeContainerIn = containerIn[yName] || {}; - var rangeContainerOut = containerOut[yName] = {}; + rangeContainerIn = containerIn[yName] || {}; + rangeContainerOut = Template.newContainer(containerOut, yName, 'yaxis'); var yAxOut = layoutOut[yName]; @@ -69,9 +72,9 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) { rangemodeDflt = 'fixed'; } - var rangeMode = coerceRange(rangeContainerIn, rangeContainerOut, 'rangemode', rangemodeDflt); + var rangeMode = coerceRange('rangemode', rangemodeDflt); if(rangeMode !== 'match') { - coerceRange(rangeContainerIn, rangeContainerOut, 'range', yAxOut.range.slice()); + coerceRange('range', yAxOut.range.slice()); } yAxOut._rangesliderAutorange = (rangeMode === 'auto'); } diff --git a/src/lib/coerce.js b/src/lib/coerce.js index bab4077e28f..a5d69fe22e2 100644 --- a/src/lib/coerce.js +++ b/src/lib/coerce.js @@ -343,12 +343,12 @@ exports.valObjectMeta = { return false; } for(var j = 0; j < v[i].length; j++) { - if(!exports.validate(v[i][j], arrayItems ? items[i][j] : items)) { + if(!validate(v[i][j], arrayItems ? items[i][j] : items)) { return false; } } } - else if(!exports.validate(v[i], arrayItems ? items[i] : items)) return false; + else if(!validate(v[i], arrayItems ? items[i] : items)) return false; } return true; @@ -369,10 +369,17 @@ exports.valObjectMeta = { * as a convenience, returns the value it finally set */ exports.coerce = function(containerIn, containerOut, attributes, attribute, dflt) { - var opts = nestedProperty(attributes, attribute).get(), - propIn = nestedProperty(containerIn, attribute), - propOut = nestedProperty(containerOut, attribute), - v = propIn.get(); + var opts = nestedProperty(attributes, attribute).get(); + var propIn = nestedProperty(containerIn, attribute); + var propOut = nestedProperty(containerOut, attribute); + var v = propIn.get(); + + var template = containerOut._template; + if(v === undefined && template) { + v = nestedProperty(template, attribute).get(); + // already used the template value, so short-circuit the second check + template = 0; + } if(dflt === undefined) dflt = opts.dflt; @@ -387,9 +394,18 @@ exports.coerce = function(containerIn, containerOut, attributes, attribute, dflt return v; } - exports.valObjectMeta[opts.valType].coerceFunction(v, propOut, dflt, opts); + var coerceFunction = exports.valObjectMeta[opts.valType].coerceFunction; + coerceFunction(v, propOut, dflt, opts); - return propOut.get(); + var out = propOut.get(); + // in case v was provided but invalid, try the template again so it still + // overrides the regular default + if(template && out === dflt && !validate(v, opts)) { + v = nestedProperty(template, attribute).get(); + coerceFunction(v, propOut, dflt, opts); + out = propOut.get(); + } + return out; }; /** @@ -486,7 +502,7 @@ exports.coerceSelectionMarkerOpacity = function(traceOut, coerce) { coerce('unselected.marker.opacity', usmoDflt); }; -exports.validate = function(value, opts) { +function validate(value, opts) { var valObjectDef = exports.valObjectMeta[opts.valType]; if(opts.arrayOk && isArrayOrTypedArray(value)) return true; @@ -503,4 +519,5 @@ exports.validate = function(value, opts) { valObjectDef.coerceFunction(value, propMock, failed, opts); return out !== failed; -}; +} +exports.validate = validate; diff --git a/src/plot_api/index.js b/src/plot_api/index.js index 1dc7da478d0..1d5f4d633ae 100644 --- a/src/plot_api/index.js +++ b/src/plot_api/index.js @@ -31,3 +31,4 @@ exports.setPlotConfig = main.setPlotConfig; exports.toImage = require('./to_image'); exports.validate = require('./validate'); exports.downloadImage = require('../snapshot/download'); +exports.makeTemplate = require('./make_template'); diff --git a/src/plot_api/make_template.js b/src/plot_api/make_template.js new file mode 100644 index 00000000000..7c4df5e1cb9 --- /dev/null +++ b/src/plot_api/make_template.js @@ -0,0 +1,210 @@ +/** +* Copyright 2012-2018, 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 isPlainObject = Lib.isPlainObject; +var PlotSchema = require('./plot_schema'); +var plotAttributes = require('../plots/attributes'); +var Template = require('./plot_template'); + +/** + * Plotly.makeTemplate: create a template off an existing figure to reuse + * style attributes on other figures. + * + * Note: separated from the rest of templates because otherwise we get circular + * references due to PlotSchema. + * + * @param {object|DOM element} figure: The figure to base the template on + * should contain a trace array `figure.data` + * and a layout object `figure.layout` + * @returns {object} template: the extracted template - can then be used as + * `layout.template` in another figure. + */ +module.exports = function makeTemplate(figure) { + var data = figure.data || []; + var layout = figure.layout || {}; + + var template = { + data: {}, + layout: {} + }; + + /* + * Note: we do NOT validate template values, we just take what's in the + * user inputs data and layout, not the validated values in fullData and + * fullLayout. Even if we were to validate here, there's no guarantee that + * these values would still be valid when applied to a new figure, which + * may contain different trace modes, different axes, etc. So it's + * important that when applying a template we still validate the template + * values, rather than just using them as defaults. + */ + + data.forEach(function(trace) { + // TODO: What if no style info is extracted for this trace. We may + // not want an empty object as the null value. + // TODO: allow transforms to contribute to templates? + // as it stands they are ignored, which may be for the best... + + var traceTemplate = {}; + walkStyleKeys(trace, traceTemplate, getTraceInfo.bind(null, trace)); + + var traceType = Lib.coerce(trace, {}, plotAttributes, 'type'); + var typeTemplates = template.data[traceType]; + if(!typeTemplates) typeTemplates = template.data[traceType] = []; + typeTemplates.push(traceTemplate); + }); + + walkStyleKeys(layout, template.layout, getLayoutInfo.bind(null, layout)); + + /* + * Compose the new template with an existing one to the same effect + * + * NOTE: there's a possibility of slightly different behavior: if the plot + * has an invalid value and the old template has a valid value for the same + * attribute, the plot will use the old template value but this routine + * will pull the invalid value (resulting in the original default). + * In the general case it's not possible to solve this with a single value, + * since valid options can be context-dependent. It could be solved with + * a *list* of values, but that would be huge complexity for little gain. + */ + var oldTemplate = layout.template; + if(isPlainObject(oldTemplate)) { + var oldLayoutTemplate = oldTemplate.layout; + + var i, traceType, oldTypeTemplates, oldTypeLen, typeTemplates, typeLen; + + if(isPlainObject(oldLayoutTemplate)) { + mergeTemplates(oldLayoutTemplate, template.layout); + } + var oldDataTemplate = oldTemplate.data; + if(isPlainObject(oldDataTemplate)) { + for(traceType in template.data) { + oldTypeTemplates = oldDataTemplate[traceType]; + if(Array.isArray(oldTypeTemplates)) { + typeTemplates = template.data[traceType]; + typeLen = typeTemplates.length; + oldTypeLen = oldTypeTemplates.length; + for(i = 0; i < typeLen; i++) { + mergeTemplates(oldTypeTemplates[i % typeLen], typeTemplates[i]); + } + for(; i < oldTypeLen; i++) { + typeTemplates.push(Lib.extendDeep({}, oldTypeTemplates[i])); + } + } + } + for(traceType in oldDataTemplate) { + if(!(traceType in template.data)) { + template.data[traceType] = Lib.extendDeep([], oldDataTemplate[traceType]); + } + } + } + } + + return template; +}; + +function mergeTemplates(oldTemplate, newTemplate) { + // we don't care about speed here, just make sure we have a totally + // distinct object from the previous template + oldTemplate = Lib.extendDeep({}, oldTemplate); + + // sort keys so we always get annotationdefaults before annotations etc + // so arrayTemplater will work right + var oldKeys = Object.keys(oldTemplate).sort(); + var i, j; + + for(i = 0; i < oldKeys.length; i++) { + var key = oldKeys[i]; + var oldVal = oldTemplate[key]; + if(key in newTemplate) { + var newVal = newTemplate[key]; + if(isPlainObject(newVal) && isPlainObject(oldVal)) { + mergeTemplates(oldVal, newVal); + } + else if(Array.isArray(newVal) && Array.isArray(oldVal)) { + var templater = Template.arrayTemplater({_template: oldTemplate}, key); + for(j = 0; j < newVal.length; j++) { + var item = newVal[j]; + var oldItem = templater.newItem(item)._template; + if(oldItem) mergeTemplates(oldItem, item); + } + var defaultItems = templater.defaultItems(); + for(j = 0; j < defaultItems.length; j++) newVal.push(defaultItems[j]); + } + } + else newTemplate[key] = oldVal; + } +} + +function walkStyleKeys(parent, templateOut, getAttributeInfo, path) { + for(var key in parent) { + var child = parent[key]; + var nextPath = getNextPath(parent, key, path); + var attr = getAttributeInfo(nextPath); + + if(!attr || + attr.valType === 'data_array' || + (attr.arrayOk && Array.isArray(child)) + ) { + continue; + } + + // TODO: special array handling wrt. defaults, named items + if(!attr.valType && isPlainObject(child)) { + walkStyleKeys(child, templateOut, getAttributeInfo, nextPath); + } + else if(attr._isLinkedToArray && Array.isArray(child)) { + var dfltDone = false; + var namedIndex = 0; + for(var i = 0; i < child.length; i++) { + var item = child[i]; + if(isPlainObject(item)) { + if(item.name) { + walkStyleKeys(item, templateOut, getAttributeInfo, + getNextPath(child, namedIndex, nextPath)); + namedIndex++; + } + else if(!dfltDone) { + var dfltKey = Template.arrayDefaultKey(key); + walkStyleKeys(item, templateOut, getAttributeInfo, + getNextPath(parent, dfltKey, path)); + dfltDone = true; + } + } + } + } + else if(attr.role === 'style') { + var templateProp = Lib.nestedProperty(templateOut, nextPath); + templateProp.set(child); + } + } +} + +function getLayoutInfo(layout, path) { + return PlotSchema.getLayoutValObject( + layout, Lib.nestedProperty({}, path).parts + ); +} + +function getTraceInfo(trace, path) { + return PlotSchema.getTraceValObject( + trace, Lib.nestedProperty({}, path).parts + ); +} + +function getNextPath(parent, key, path) { + var nextPath; + if(!path) nextPath = key; + else if(Array.isArray(parent)) nextPath = path + '[' + key + ']'; + else nextPath = path + '.' + key; + + return nextPath; +} diff --git a/src/plot_api/plot_template.js b/src/plot_api/plot_template.js new file mode 100644 index 00000000000..5821060e6a0 --- /dev/null +++ b/src/plot_api/plot_template.js @@ -0,0 +1,183 @@ +/** +* Copyright 2012-2018, 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 plotAttributes = require('../plots/attributes'); + +var TEMPLATEITEMNAME = exports.TEMPLATEITEMNAME = 'templateitemname'; + +/** + * traceTemplater: logic for matching traces to trace templates + * + * @param {object} dataTemplate: collection of {traceType: [{template}, ...]} + * ie each type the template applies to contains a list of template objects, + * to be provided cyclically to data traces of that type. + * + * @returns {object}: {newTrace}, a function: + * newTrace(traceIn): that takes the input traceIn, coerces its type, then + * uses that type to find the next template to apply. returns the output + * traceOut with template attached, ready to continue supplyDefaults. + */ +exports.traceTemplater = function(dataTemplate) { + var traceCounts = {}; + var traceType, typeTemplates; + + for(traceType in dataTemplate) { + typeTemplates = dataTemplate[traceType]; + if(Array.isArray(typeTemplates) && typeTemplates.length) { + traceCounts[traceType] = 0; + } + } + + function newTrace(traceIn) { + traceType = Lib.coerce(traceIn, {}, plotAttributes, 'type'); + var traceOut = {type: traceType, _template: null}; + if(traceType in traceCounts) { + typeTemplates = dataTemplate[traceType]; + // cycle through traces in the template set for this type + var typei = traceCounts[traceType] % typeTemplates.length; + traceCounts[traceType]++; + traceOut._template = typeTemplates[typei]; + } + else { + // TODO: anything we should do for types missing from the template? + // try to apply some other type? Or just bail as we do here? + // Actually I think yes, we should apply other types; would be nice + // if all scatter* could inherit from each other, and if histogram + // could inherit from bar, etc... but how to specify this? And do we + // compose them, or if a type is present require it to be complete? + // Actually this could apply to layout too - 3D annotations + // inheriting from 2D, axes of different types inheriting from each + // other... + } + return traceOut; + } + + return { + newTrace: newTrace + // TODO: function to figure out what's left & what didn't work + }; +}; + +/** + * newContainer: Create a new sub-container inside `container` and propagate any + * applicable template to it. If there's no template, still propagates + * `undefined` so relinkPrivate will not retain an old template! + * + * @param {object} container: the outer container, should already have _template + * if there *is* a template for this plot + * @param {string} name: the key of the new container to make + * @param {string} baseName: if applicable, a base attribute to take the + * template from, ie for xaxis3 the base would be xaxis + * + * @returns {object}: an object for inclusion _full*, empty except for the + * appropriate template piece + */ +exports.newContainer = function(container, name, baseName) { + var template = container._template; + var part = template && (template[name] || (baseName && template[baseName])); + if(!Lib.isPlainObject(part)) part = null; + + var out = container[name] = {_template: part}; + return out; +}; + +/** + * arrayTemplater: special logic for templating both defaults and specific items + * in a container array (annotations etc) + * + * @param {object} container: the outer container, should already have _template + * if there *is* a template for this plot + * @param {string} name: the name of the array to template (ie 'annotations') + * will be used to find default ('annotationdefaults' object) and specific + * ('annotations' array) template specs. + * + * @returns {object}: {newItem, defaultItems}, both functions: + * newItem(itemIn): create an output item, bare except for the correct + * template and name(s), as the base for supplyDefaults + * defaultItems(): to be called after all newItem calls, return any + * specific template items that have not already beeen included, + * also as bare output items ready for supplyDefaults. + */ +exports.arrayTemplater = function(container, name) { + var template = container._template; + var defaultsTemplate = template && template[arrayDefaultKey(name)]; + var templateItems = template && template[name]; + if(!Array.isArray(templateItems) || !templateItems.length) { + templateItems = []; + } + + var usedIndices = {}; + + function newItem(itemIn) { + // include name and templateitemname in the output object for ALL + // container array items. Note: you could potentially use different + // name and templateitemname, if you're using one template to make + // another template. templateitemname would be the name in the original + // template, and name is the new "subclassed" item name. + var out = {name: itemIn.name}; + var templateItemName = out[TEMPLATEITEMNAME] = itemIn[TEMPLATEITEMNAME]; + + // no itemname: use the default template + if(!templateItemName) { + out._template = defaultsTemplate; + return out; + } + + // look for an item matching this itemname + // note these do not inherit from the default template, only the item. + for(var i = 0; i < templateItems.length; i++) { + var templateItem = templateItems[i]; + if(templateItem.name === templateItemName) { + // Note: it's OK to use a template item more than once + // but using it at least once will stop it from generating + // a default item at the end. + usedIndices[i] = 1; + out._template = templateItem; + return out; + } + } + + // Didn't find a matching template item, so since this item is intended + // to only be modifications it's most likely broken. Hide it unless + // it's explicitly marked visible - in which case it gets NO template, + // not even the default. + out.visible = itemIn.visible || false; + return out; + } + + function defaultItems() { + var out = []; + for(var i = 0; i < templateItems.length; i++) { + if(!usedIndices[i]) { + var templateItem = templateItems[i]; + var outi = {_template: templateItem, name: templateItem.name}; + outi[TEMPLATEITEMNAME] = templateItem[TEMPLATEITEMNAME]; + out.push(outi); + } + } + return out; + } + + return { + newItem: newItem, + defaultItems: defaultItems + }; +}; + +function arrayDefaultKey(name) { + var lastChar = name.length - 1; + if(name.charAt(lastChar) !== 's') { + Lib.warn('bad argument to arrayDefaultKey: ' + name); + } + return name.substr(0, name.length - 1) + 'defaults'; +} +exports.arrayDefaultKey = arrayDefaultKey; diff --git a/src/plot_api/validate.js b/src/plot_api/validate.js index ae789027bf5..0baf1649182 100644 --- a/src/plot_api/validate.js +++ b/src/plot_api/validate.js @@ -235,7 +235,12 @@ function crawl(objIn, objOut, schema, list, base, path) { if(isPlainObject(valIn[_index]) && isPlainObject(valOut[j])) { indexList.push(_index); - crawl(valIn[_index], valOut[j], _nestedSchema, list, base, _p); + var valInj = valIn[_index]; + var valOutj = valOut[j]; + if(isPlainObject(valInj) && valInj.visible !== false && valOutj.visible === false) { + list.push(format('invisible', base, _p)); + } + else crawl(valInj, valOutj, _nestedSchema, list, base, _p); } } @@ -327,8 +332,10 @@ var code2msgFunc = { 'during defaults.' ].join(' '); }, - invisible: function(base) { - return 'Trace ' + base[1] + ' got defaulted to be not visible'; + invisible: function(base, astr) { + return ( + astr ? (inBase(base) + 'item ' + astr) : ('Trace ' + base[1]) + ) + ' got defaulted to be not visible'; }, value: function(base, astr, valIn) { return [ diff --git a/src/plots/array_container_defaults.js b/src/plots/array_container_defaults.js index 6c648d7bbdd..b7e20ecb1f7 100644 --- a/src/plots/array_container_defaults.js +++ b/src/plots/array_container_defaults.js @@ -9,6 +9,7 @@ 'use strict'; var Lib = require('../lib'); +var Template = require('../plot_api/plot_template'); /** Convenience wrapper for making array container logic DRY and consistent * @@ -46,21 +47,24 @@ module.exports = function handleArrayContainerDefaults(parentObjIn, parentObjOut var previousContOut = parentObjOut[name]; - var contIn = Lib.isArrayOrTypedArray(parentObjIn[name]) ? parentObjIn[name] : [], - contOut = parentObjOut[name] = [], - i; + var contIn = Lib.isArrayOrTypedArray(parentObjIn[name]) ? parentObjIn[name] : []; + var contOut = parentObjOut[name] = []; + var templater = Template.arrayTemplater(parentObjOut, name); + var i, itemOut; for(i = 0; i < contIn.length; i++) { var itemIn = contIn[i]; - var itemOut = {}; - var itemOpts = {index: i}; + var itemOpts = {}; if(!Lib.isPlainObject(itemIn)) { itemOpts.itemIsNotPlainObject = true; itemIn = {}; } + itemOut = templater.newItem(itemIn); - opts.handleItemDefaults(itemIn, itemOut, parentObjOut, opts, itemOpts); + if(itemOut.visible !== false) { + opts.handleItemDefaults(itemIn, itemOut, parentObjOut, opts, itemOpts); + } itemOut._input = itemIn; itemOut._index = i; @@ -68,6 +72,16 @@ module.exports = function handleArrayContainerDefaults(parentObjIn, parentObjOut contOut.push(itemOut); } + var defaultItems = templater.defaultItems(); + for(i = 0; i < defaultItems.length; i++) { + itemOut = defaultItems[i]; + opts.handleItemDefaults({}, itemOut, parentObjOut, opts, {}); + // TODO: we don't have an _input here - need special handling for edits, + // is that all _input is used for? + itemOut._index = contOut.length; + 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.isArrayOrTypedArray(previousContOut)) { @@ -76,4 +90,6 @@ module.exports = function handleArrayContainerDefaults(parentObjIn, parentObjOut Lib.relinkPrivateKeys(contOut[i], previousContOut[i]); } } + + return contOut; }; diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index adb92edd49a..f17c71f7f19 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -12,6 +12,7 @@ var Registry = require('../../registry'); var Lib = require('../../lib'); var Color = require('../../components/color'); +var Template = require('../../plot_api/plot_template'); var basePlotLayoutAttributes = require('../layout_attributes'); var layoutAttributes = require('./layout_attributes'); @@ -117,17 +118,17 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { // first pass creates the containers, determines types, and handles most of the settings for(i = 0; i < axNames.length; i++) { axName = axNames[i]; + axLetter = axName.charAt(0); if(!Lib.isPlainObject(layoutIn[axName])) { layoutIn[axName] = {}; } axLayoutIn = layoutIn[axName]; - axLayoutOut = layoutOut[axName] = {}; + axLayoutOut = Template.newContainer(layoutOut, axName, axLetter + 'axis'); handleTypeDefaults(axLayoutIn, axLayoutOut, coerce, fullData, axName); - axLetter = axName.charAt(0); var overlayableAxes = getOverlayableAxes(axLetter, axName); var defaultOptions = { diff --git a/src/plots/gl3d/layout/axis_defaults.js b/src/plots/gl3d/layout/axis_defaults.js index 7437054c7f7..a3ac9c14285 100644 --- a/src/plots/gl3d/layout/axis_defaults.js +++ b/src/plots/gl3d/layout/axis_defaults.js @@ -12,6 +12,7 @@ var colorMix = require('tinycolor2').mix; var Lib = require('../../../lib'); +var Template = require('../../../plot_api/plot_template'); var layoutAttributes = require('./axis_attributes'); var handleTypeDefaults = require('../../cartesian/type_defaults'); @@ -34,10 +35,9 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, options) { var axName = axesNames[j]; containerIn = layoutIn[axName] || {}; - containerOut = layoutOut[axName] = { - _id: axName[0] + options.scene, - _name: axName - }; + containerOut = Template.newContainer(layoutOut, axName); + containerOut._id = axName[0] + options.scene; + containerOut._name = axName; handleTypeDefaults(containerIn, containerOut, coerce, options.data); diff --git a/src/plots/layout_attributes.js b/src/plots/layout_attributes.js index 8add2156942..4b320e18d6b 100644 --- a/src/plots/layout_attributes.js +++ b/src/plots/layout_attributes.js @@ -195,5 +195,28 @@ module.exports = { 'being treated as immutable, thus any data array with a', 'different identity from its predecessor contains new data.' ].join(' ') + }, + template: { + valType: 'any', + role: 'info', + editType: 'calc', + description: [ + 'Default attributes to be applied to the plot. Templates can be', + 'created from existing plots using `Plotly.makeTemplate`, or', + 'created manually. They should be objects with format:', + '`{layout: layoutTemplate, data: {[type]: [traceTemplate, ...]}, ...}`', + '`layoutTemplate` and `traceTemplate` are objects matching the', + 'attribute structure of `layout` and a data trace. ', + 'Trace templates are applied cyclically to traces of each type.', + 'Container arrays (eg `annotations`) have special handling:', + 'An object ending in `defaults` (eg `annotationdefaults`) is applied', + 'to each array item. But if an item has a `templateitemname` key', + 'we look in the template array for an item with matching `name` and', + 'apply that instead. If no matching `name` is found we mark the item', + 'invisible. Any named template item not referenced is appended to', + 'the end of the array, so you can use this for a watermark annotation', + 'or a logo image, for example. To omit one of these items on the plot,', + 'make an item with matching `templateitemname` and `visible: false`.' + ].join(' ') } }; diff --git a/src/plots/plots.js b/src/plots/plots.js index d144beaf9a8..176358c0f10 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -14,6 +14,7 @@ var isNumeric = require('fast-isnumeric'); var Registry = require('../registry'); var PlotSchema = require('../plot_api/plot_schema'); +var Template = require('../plot_api/plot_template'); var Lib = require('../lib'); var Color = require('../components/color'); var BADNUM = require('../constants/numerical').BADNUM; @@ -928,12 +929,17 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { var carpetIndex = {}; var carpetDependents = []; + var dataTemplate = (layout.template || {}).data || {}; + var templater = Template.traceTemplater(dataTemplate); for(i = 0; i < dataIn.length; i++) { trace = dataIn[i]; - fullTrace = plots.supplyTraceDefaults(trace, colorCnt, fullLayout, i, - // reuse uid we may have pulled out of oldFullData - fullLayout._traceUids[i]); + + // reuse uid we may have pulled out of oldFullData + // Note: templater supplies trace type + fullTrace = templater.newTrace(trace); + fullTrace.uid = fullLayout._traceUids[i]; + plots.supplyTraceDefaults(trace, fullTrace, colorCnt, fullLayout, i); fullTrace.uid = fullLayout._traceUids[i]; @@ -946,12 +952,16 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { for(var j = 0; j < expandedTraces.length; j++) { var expandedTrace = expandedTraces[j]; - var fullExpandedTrace = plots.supplyTraceDefaults( - expandedTrace, cnt, fullLayout, i, + + // No further templating during transforms. + var fullExpandedTrace = { + _template: fullTrace._template, + type: fullTrace.type, // set uid using parent uid and expanded index // to promote consistency between update calls - fullTrace.uid + j - ); + uid: fullTrace.uid + j + }; + plots.supplyTraceDefaults(expandedTrace, fullExpandedTrace, cnt, fullLayout, i); // relink private (i.e. underscore) keys expanded trace to full expanded trace so // that transform supply-default methods can set _ keys for future use. @@ -1081,9 +1091,8 @@ plots.supplyFrameDefaults = function(frameIn) { return frameOut; }; -plots.supplyTraceDefaults = function(traceIn, colorIndex, layout, traceInIndex, uid) { +plots.supplyTraceDefaults = function(traceIn, traceOut, colorIndex, layout, traceInIndex) { var colorway = layout.colorway || Color.defaults; - var traceOut = {uid: uid}; var defaultColor = colorway[colorIndex % colorway.length]; var i; @@ -1268,6 +1277,13 @@ plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut, formatObj) { return Lib.coerce(layoutIn, layoutOut, plots.layoutAttributes, attr, dflt); } + var template = layoutIn.template; + if(Lib.isPlainObject(template)) { + layoutOut.template = template; + layoutOut._template = template.layout; + layoutOut._dataTemplate = template.data; + } + var globalFont = Lib.coerceFont(coerce, 'font'); coerce('title', layoutOut._dfltTitle.plot); diff --git a/src/plots/polar/layout_defaults.js b/src/plots/polar/layout_defaults.js index 19bf636326a..5c103448f72 100644 --- a/src/plots/polar/layout_defaults.js +++ b/src/plots/polar/layout_defaults.js @@ -49,6 +49,10 @@ function handleDefaults(contIn, contOut, coerce, opts) { } var axIn = contIn[axName]; + // Note: does not need template propagation, since coerceAxis is still + // based on the subplot-wide coerce function. Though it may be more + // efficient to make a new coerce function, then we *would* need to + // propagate the template. var axOut = contOut[axName] = {}; axOut._id = axOut._name = axName; diff --git a/src/plots/subplot_defaults.js b/src/plots/subplot_defaults.js index b8c229cc8a1..9e83049b773 100644 --- a/src/plots/subplot_defaults.js +++ b/src/plots/subplot_defaults.js @@ -10,6 +10,7 @@ 'use strict'; var Lib = require('../lib'); +var Template = require('../plot_api/plot_template'); var handleDomainDefaults = require('./domain').defaults; @@ -49,6 +50,8 @@ module.exports = function handleSubplotDefaults(layoutIn, layoutOut, fullData, o var ids = layoutOut._subplots[subplotType]; var idsLength = ids.length; + var baseId = idsLength && ids[0].replace(/\d+$/, ''); + var subplotLayoutIn, subplotLayoutOut; function coerce(attr, dflt) { @@ -62,7 +65,7 @@ module.exports = function handleSubplotDefaults(layoutIn, layoutOut, fullData, o if(layoutIn[id]) subplotLayoutIn = layoutIn[id]; else subplotLayoutIn = layoutIn[id] = {}; - layoutOut[id] = subplotLayoutOut = {}; + subplotLayoutOut = Template.newContainer(layoutOut, id, baseId); var dfltDomains = {}; dfltDomains[partition] = [i / idsLength, (i + 1) / idsLength]; diff --git a/src/plots/ternary/layout/defaults.js b/src/plots/ternary/layout/defaults.js index 50665b1b9f2..12bcdf22499 100644 --- a/src/plots/ternary/layout/defaults.js +++ b/src/plots/ternary/layout/defaults.js @@ -10,6 +10,7 @@ 'use strict'; var Color = require('../../../components/color'); +var Template = require('../../../plot_api/plot_template'); var handleSubplotDefaults = require('../../subplot_defaults'); var layoutAttributes = require('./layout_attributes'); @@ -39,7 +40,8 @@ function handleTernaryDefaults(ternaryLayoutIn, ternaryLayoutOut, coerce, option for(var j = 0; j < axesNames.length; j++) { axName = axesNames[j]; containerIn = ternaryLayoutIn[axName] || {}; - containerOut = ternaryLayoutOut[axName] = {_name: axName, type: 'linear'}; + containerOut = Template.newContainer(ternaryLayoutOut, axName); + containerOut._name = axName; handleAxisDefaults(containerIn, containerOut, options); } diff --git a/test/jasmine/tests/annotations_test.js b/test/jasmine/tests/annotations_test.js index 906e354654b..7054a792a92 100644 --- a/test/jasmine/tests/annotations_test.js +++ b/test/jasmine/tests/annotations_test.js @@ -55,12 +55,12 @@ describe('Test annotations', function() { expect(layoutIn.annotations).toEqual(annotations); out.forEach(function(item, i) { - expect(item).toEqual({ + expect(item).toEqual(jasmine.objectContaining({ visible: false, _input: {}, _index: i, clicktoshow: false - }); + })); }); }); diff --git a/test/jasmine/tests/heatmap_test.js b/test/jasmine/tests/heatmap_test.js index 19fd83e48e3..c65a7987043 100644 --- a/test/jasmine/tests/heatmap_test.js +++ b/test/jasmine/tests/heatmap_test.js @@ -55,13 +55,13 @@ describe('heatmap supplyDefaults', function() { type: 'heatmap', z: [[1, 2], []] }; - traceOut = Plots.supplyTraceDefaults(traceIn, 0, layout); + traceOut = Plots.supplyTraceDefaults(traceIn, {type: 'heatmap'}, 0, layout); traceIn = { type: 'heatmap', z: [[], [1, 2], [1, 2, 3]] }; - traceOut = Plots.supplyTraceDefaults(traceIn, 0, layout); + traceOut = Plots.supplyTraceDefaults(traceIn, {type: 'heatmap'}, 0, layout); expect(traceOut.visible).toBe(true); expect(traceOut.visible).toBe(true); }); diff --git a/test/jasmine/tests/layout_images_test.js b/test/jasmine/tests/layout_images_test.js index fca0b88747f..ffd48139b1d 100644 --- a/test/jasmine/tests/layout_images_test.js +++ b/test/jasmine/tests/layout_images_test.js @@ -32,11 +32,11 @@ describe('Layout images', function() { Images.supplyLayoutDefaults(layoutIn, layoutOut); - expect(layoutOut.images).toEqual([{ + expect(layoutOut.images).toEqual([jasmine.objectContaining({ visible: false, _index: 0, _input: layoutIn.images[0] - }]); + })]); }); it('should reject when not an array', function() { @@ -77,7 +77,7 @@ describe('Layout images', function() { Images.supplyLayoutDefaults(layoutIn, layoutOut); - expect(layoutOut.images[0]).toEqual(expected); + expect(layoutOut.images[0]).toEqual(jasmine.objectContaining(expected)); }); }); diff --git a/test/jasmine/tests/plots_test.js b/test/jasmine/tests/plots_test.js index c048ceec8be..244b3dccf6d 100644 --- a/test/jasmine/tests/plots_test.js +++ b/test/jasmine/tests/plots_test.js @@ -253,23 +253,23 @@ describe('Test Plots', function() { layout._dataLength = 1; traceIn = {}; - traceOut = supplyTraceDefaults(traceIn, 0, layout); + traceOut = supplyTraceDefaults(traceIn, {type: 'scatter'}, 0, layout); expect(traceOut.hoverinfo).toEqual('x+y+z+text'); traceIn = { hoverinfo: 'name' }; - traceOut = supplyTraceDefaults(traceIn, 0, layout); + traceOut = supplyTraceDefaults(traceIn, {type: 'scatter'}, 0, layout); expect(traceOut.hoverinfo).toEqual('name'); }); - it('without *name* for single-trace graphs by default', function() { + it('with *name* for multi-trace graphs by default', function() { layout._dataLength = 2; traceIn = {}; - traceOut = supplyTraceDefaults(traceIn, 0, layout); + traceOut = supplyTraceDefaults(traceIn, {type: 'scatter'}, 0, layout); expect(traceOut.hoverinfo).toEqual('all'); traceIn = { hoverinfo: 'name' }; - traceOut = supplyTraceDefaults(traceIn, 0, layout); + traceOut = supplyTraceDefaults(traceIn, {type: 'scatter'}, 0, layout); expect(traceOut.hoverinfo).toEqual('name'); }); }); diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index 9eea7bbecdf..6ba00809e06 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -562,7 +562,7 @@ describe('Rangeslider handleDefaults function', function() { }; _supply(layoutIn, layoutOut, 'xaxis'); - expect(layoutOut.xaxis.rangeslider).toEqual(expected); + expect(layoutOut.xaxis.rangeslider).toEqual(jasmine.objectContaining(expected)); }); it('should set defaults if rangeslider is requested', function() { @@ -583,7 +583,7 @@ describe('Rangeslider handleDefaults function', function() { // but that's a problem for another time. // see https://github.com/plotly/plotly.js/issues/1473 expect(layoutIn).toEqual({xaxis: {rangeslider: {}}}); - expect(layoutOut.xaxis.rangeslider).toEqual(expected); + expect(layoutOut.xaxis.rangeslider).toEqual(jasmine.objectContaining(expected)); }); it('should set defaults if rangeslider.visible is true', function() { @@ -600,7 +600,7 @@ describe('Rangeslider handleDefaults function', function() { }; _supply(layoutIn, layoutOut, 'xaxis'); - expect(layoutOut.xaxis.rangeslider).toEqual(expected); + expect(layoutOut.xaxis.rangeslider).toEqual(jasmine.objectContaining(expected)); }); it('should return early if *visible: false*', function() { @@ -608,7 +608,7 @@ describe('Rangeslider handleDefaults function', function() { layoutOut = { xaxis: { rangeslider: {}} }; _supply(layoutIn, layoutOut, 'xaxis'); - expect(layoutOut.xaxis.rangeslider).toEqual({ visible: false }); + expect(layoutOut.xaxis.rangeslider).toEqual(jasmine.objectContaining({ visible: false })); }); it('should set defaults if properties are invalid', function() { @@ -631,7 +631,7 @@ describe('Rangeslider handleDefaults function', function() { }; _supply(layoutIn, layoutOut, 'xaxis'); - expect(layoutOut.xaxis.rangeslider).toEqual(expected); + expect(layoutOut.xaxis.rangeslider).toEqual(jasmine.objectContaining(expected)); }); it('should set autorange to true when range input is invalid', function() { @@ -648,7 +648,7 @@ describe('Rangeslider handleDefaults function', function() { }; _supply(layoutIn, layoutOut, 'xaxis'); - expect(layoutOut.xaxis.rangeslider).toEqual(expected); + expect(layoutOut.xaxis.rangeslider).toEqual(jasmine.objectContaining(expected)); }); it('should default \'bgcolor\' to layout \'plot_bgcolor\'', function() { @@ -678,7 +678,7 @@ describe('Rangeslider yaxis options', function() { supplyAllDefaults(mock); - expect(mock._fullLayout.xaxis.rangeslider.yaxis).toEqual({ rangemode: 'match' }); + expect(mock._fullLayout.xaxis.rangeslider.yaxis).toEqual(jasmine.objectContaining({ rangemode: 'match' })); }); it('should set multiple yaxis with data are present', function() { @@ -697,8 +697,8 @@ describe('Rangeslider yaxis options', function() { supplyAllDefaults(mock); - expect(mock._fullLayout.xaxis.rangeslider.yaxis).toEqual({ rangemode: 'match' }); - expect(mock._fullLayout.xaxis.rangeslider.yaxis2).toEqual({ rangemode: 'match' }); + expect(mock._fullLayout.xaxis.rangeslider.yaxis).toEqual(jasmine.objectContaining({ rangemode: 'match' })); + expect(mock._fullLayout.xaxis.rangeslider.yaxis2).toEqual(jasmine.objectContaining({ rangemode: 'match' })); expect(mock._fullLayout.xaxis.rangeslider.yaxis3).toEqual(undefined); }); @@ -723,9 +723,9 @@ describe('Rangeslider yaxis options', function() { supplyAllDefaults(mock); - expect(mock._fullLayout.xaxis.rangeslider.yaxis).toEqual({ rangemode: 'auto', range: [-1, 4] }); - expect(mock._fullLayout.xaxis.rangeslider.yaxis2).toEqual({ rangemode: 'fixed', range: [-1, 4] }); - expect(mock._fullLayout.xaxis.rangeslider.yaxis3).toEqual({ rangemode: 'fixed', range: [0, 1] }); + expect(mock._fullLayout.xaxis.rangeslider.yaxis).toEqual(jasmine.objectContaining({ rangemode: 'auto', range: [-1, 4] })); + expect(mock._fullLayout.xaxis.rangeslider.yaxis2).toEqual(jasmine.objectContaining({ rangemode: 'fixed', range: [-1, 4] })); + expect(mock._fullLayout.xaxis.rangeslider.yaxis3).toEqual(jasmine.objectContaining({ rangemode: 'fixed', range: [0, 1] })); }); }); @@ -911,7 +911,7 @@ describe('rangesliders in general', function() { xaxis: { rangeslider: { yaxis: { range: [-10, 20] } } } }) .then(function() { - expect(gd.layout.xaxis.rangeslider.yaxis).toEqual({ rangemode: 'fixed', range: [-10, 20] }); + expect(gd.layout.xaxis.rangeslider.yaxis).toEqual(jasmine.objectContaining({ rangemode: 'fixed', range: [-10, 20] })); return Plotly.relayout(gd, 'xaxis.rangeslider.yaxis.rangemode', 'auto'); }) @@ -925,7 +925,7 @@ describe('rangesliders in general', function() { return Plotly.relayout(gd, 'xaxis.rangeslider.yaxis.rangemode', 'match'); }) .then(function() { - expect(gd.layout.xaxis.rangeslider.yaxis).toEqual({ rangemode: 'match' }); + expect(gd.layout.xaxis.rangeslider.yaxis).toEqual(jasmine.objectContaining({ rangemode: 'match' })); }) .catch(failTest) .then(done); diff --git a/test/jasmine/tests/shapes_test.js b/test/jasmine/tests/shapes_test.js index 66aa2c53dc6..4a0cd164b25 100644 --- a/test/jasmine/tests/shapes_test.js +++ b/test/jasmine/tests/shapes_test.js @@ -77,11 +77,11 @@ describe('Test shapes defaults:', function() { expect(layoutIn.shapes).toEqual(shapes); out.forEach(function(item, i) { - expect(item).toEqual({ + expect(item).toEqual(jasmine.objectContaining({ visible: false, _input: {}, _index: i - }); + })); }); }); diff --git a/test/jasmine/tests/template_test.js b/test/jasmine/tests/template_test.js new file mode 100644 index 00000000000..6fa22769479 --- /dev/null +++ b/test/jasmine/tests/template_test.js @@ -0,0 +1,41 @@ +var Plotly = require('@lib/index'); +var Lib = require('@src/lib'); + +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); +var fail = require('../assets/fail_test'); + +var scatterFill = require('@mocks/scatter_fill_self_next.json'); + + +describe('makeTemplate', function() { + it('does not template non-style keys', function() { + var template = Plotly.makeTemplate(scatterFill); + expect(template).toEqual({ + data: [{fill: 'tonext'}, {fill: 'tonext'}, {fill: 'toself'}] + }); + }); + it('does not template empty layout', function() { + var template = Plotly.makeTemplate(scatterFill); + expect(template.layout).toBe(undefined); + }); + it('templates scalar array_ok keys but not when they are arrays', function() { + var figure = {data: [{marker: {color: 'red'}}]}; + var template = Plotly.makeTemplate(figure); + expect(template.data[0].marker.color).toBe('red'); + + }); +}); + +describe('applyTemplate', function() { + it('forces default values for keys not present in template', function() { + var template = { + data: [{fill: 'tonext'}, {fill: 'tonext'}, {fill: 'toself'}] + }; + var figure = Lib.extendDeepAll({}, scatterFill); + var results = Plotly.applyTemplate(figure, template); + var templatedFigure = results.templatedFigure; + + expect(templatedFigure).toEqual(figure); + }); +}); diff --git a/test/jasmine/tests/transform_filter_test.js b/test/jasmine/tests/transform_filter_test.js index e518d9080f2..a07ba5f555b 100644 --- a/test/jasmine/tests/transform_filter_test.js +++ b/test/jasmine/tests/transform_filter_test.js @@ -31,7 +31,7 @@ describe('filter transforms defaults:', function() { }] }; - traceOut = Plots.supplyTraceDefaults(traceIn, 0, fullLayout); + traceOut = Plots.supplyTraceDefaults(traceIn, {type: 'scatter'}, 0, fullLayout); expect(traceOut.transforms).toEqual([{ type: 'filter', @@ -54,7 +54,7 @@ describe('filter transforms defaults:', function() { }] }; - traceOut = Plots.supplyTraceDefaults(traceIn, 0, fullLayout); + traceOut = Plots.supplyTraceDefaults(traceIn, {type: 'scatter'}, 0, fullLayout); expect(traceOut.transforms).toEqual([{ type: 'filter', @@ -80,7 +80,7 @@ describe('filter transforms defaults:', function() { }] }; - traceOut = Plots.supplyTraceDefaults(traceIn, 0, fullLayout); + traceOut = Plots.supplyTraceDefaults(traceIn, {type: 'scatter'}, 0, fullLayout); expect(traceOut.transforms[0].target).toEqual('x'); expect(traceOut.transforms[1].target).toEqual('x'); diff --git a/test/jasmine/tests/transform_multi_test.js b/test/jasmine/tests/transform_multi_test.js index 2d946d383eb..9193794c9d7 100644 --- a/test/jasmine/tests/transform_multi_test.js +++ b/test/jasmine/tests/transform_multi_test.js @@ -41,7 +41,7 @@ describe('general transforms:', function() { transforms: [{}] }; - traceOut = Plots.supplyTraceDefaults(traceIn, 0, fullLayout); + traceOut = Plots.supplyTraceDefaults(traceIn, {type: 'scatter'}, 0, fullLayout); expect(traceOut.transforms).toEqual([{}]); }); @@ -52,7 +52,7 @@ describe('general transforms:', function() { transforms: [{}] }; - traceOut = Plots.supplyTraceDefaults(traceIn, 0, fullLayout); + traceOut = Plots.supplyTraceDefaults(traceIn, {type: 'scatter'}, 0, fullLayout); expect(traceOut.transforms).toBeUndefined(); }); @@ -63,7 +63,7 @@ describe('general transforms:', function() { transforms: [{ type: 'filter' }] }; - traceOut = Plots.supplyTraceDefaults(traceIn, 0, fullLayout); + traceOut = Plots.supplyTraceDefaults(traceIn, {type: 'scatter'}, 0, fullLayout); expect(traceOut.transforms).toEqual([{ type: 'filter', @@ -82,7 +82,7 @@ describe('general transforms:', function() { transforms: [{ type: 'invalid' }] }; - traceOut = Plots.supplyTraceDefaults(traceIn, 0, fullLayout); + traceOut = Plots.supplyTraceDefaults(traceIn, {type: 'scatter'}, 0, fullLayout); expect(traceOut.y).toBe(traceIn.y); }); @@ -108,7 +108,7 @@ describe('general transforms:', function() { _basePlotModules: [] }; - traceOut = Plots.supplyTraceDefaults(traceIn, 0, layout); + traceOut = Plots.supplyTraceDefaults(traceIn, {type: 'scatter'}, 0, layout); expect(traceOut.transforms[0]).toEqual(jasmine.objectContaining({ type: 'filter', diff --git a/test/jasmine/tests/transform_sort_test.js b/test/jasmine/tests/transform_sort_test.js index 2f6bfdfcc62..fca82e09c24 100644 --- a/test/jasmine/tests/transform_sort_test.js +++ b/test/jasmine/tests/transform_sort_test.js @@ -17,7 +17,7 @@ describe('Test sort transform defaults:', function() { _modules: [], _basePlotModules: [] }); - return Plots.supplyTraceDefaults(trace, 0, layout); + return Plots.supplyTraceDefaults(trace, {type: trace.type || 'scatter'}, 0, layout); } it('should coerce all attributes', function() { diff --git a/test/jasmine/tests/validate_test.js b/test/jasmine/tests/validate_test.js index cd868ee9782..4d1d246ddb0 100644 --- a/test/jasmine/tests/validate_test.js +++ b/test/jasmine/tests/validate_test.js @@ -282,20 +282,25 @@ describe('Plotly.validate', function() { 'In layout, key shapes[0].opacity is set to an invalid value (none)' ); assertErrorContent( - out[8], 'schema', 'layout', null, - ['updatemenus', 2, 'buttons', 1, 'title'], 'updatemenus[2].buttons[1].title', - 'In layout, key updatemenus[2].buttons[1].title is not part of the schema' + out[8], 'invisible', 'layout', null, + ['updatemenus', 2, 'buttons', 0], 'updatemenus[2].buttons[0]', + 'In layout, item updatemenus[2].buttons[0] got defaulted to be not visible' ); assertErrorContent( - out[9], 'unused', 'layout', null, - ['updatemenus', 2, 'buttons', 0], 'updatemenus[2].buttons[0]', - 'In layout, key updatemenus[2].buttons[0] did not get coerced' + out[9], 'schema', 'layout', null, + ['updatemenus', 2, 'buttons', 1, 'title'], 'updatemenus[2].buttons[1].title', + 'In layout, key updatemenus[2].buttons[1].title is not part of the schema' ); assertErrorContent( out[10], 'object', 'layout', null, ['updatemenus', 2, 'buttons', 2], 'updatemenus[2].buttons[2]', 'In layout, key updatemenus[2].buttons[2] must be linked to an object container' ); + assertErrorContent( + out[11], 'object', 'layout', null, + ['updatemenus', 1], 'updatemenus[1]', + 'In layout, key updatemenus[1] must be linked to an object container' + ); }); it('should work with isSubplotObj attributes', function() { From b3da4e71b46be44918731aee8eb17e3ef7bc37d0 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 26 Jun 2018 16:51:11 -0400 Subject: [PATCH 08/24] :hocho: itemIsNotPlainObject from handleArrayContainerDefaults --- src/components/annotations/defaults.js | 8 ++--- src/components/annotations3d/defaults.js | 8 ++--- .../rangeselector/button_attributes.js | 1 + src/components/rangeselector/defaults.js | 30 +++++++++---------- src/components/shapes/defaults.js | 8 ++--- src/components/sliders/defaults.js | 15 +++++----- src/components/updatemenus/defaults.js | 4 +-- src/plots/array_container_defaults.js | 13 ++++---- src/plots/cartesian/tick_label_defaults.js | 4 +-- src/plots/mapbox/layout_defaults.js | 4 +-- src/traces/parcoords/defaults.js | 4 +-- src/traces/splom/defaults.js | 8 ++--- test/jasmine/tests/annotations_test.js | 4 +-- test/jasmine/tests/shapes_test.js | 1 - test/jasmine/tests/updatemenus_test.js | 2 -- 15 files changed, 52 insertions(+), 62 deletions(-) diff --git a/src/components/annotations/defaults.js b/src/components/annotations/defaults.js index 07ad6ae4c5a..2ca55c44bae 100644 --- a/src/components/annotations/defaults.js +++ b/src/components/annotations/defaults.js @@ -24,15 +24,15 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut) { }); }; -function handleAnnotationDefaults(annIn, annOut, fullLayout, opts, itemOpts) { +function handleAnnotationDefaults(annIn, annOut, fullLayout) { function coerce(attr, dflt) { return Lib.coerce(annIn, annOut, attributes, attr, dflt); } - var visible = coerce('visible', !itemOpts.itemIsNotPlainObject); + var visible = coerce('visible'); var clickToShow = coerce('clicktoshow'); - if(!(visible || clickToShow)) return annOut; + if(!(visible || clickToShow)) return; handleAnnotationCommonDefaults(annIn, annOut, fullLayout, coerce); @@ -96,6 +96,4 @@ function handleAnnotationDefaults(annIn, annOut, fullLayout, opts, itemOpts) { annOut.y : Axes.cleanPosition(yClick, gdMock, annOut.yref); } - - return annOut; } diff --git a/src/components/annotations3d/defaults.js b/src/components/annotations3d/defaults.js index 290d6c2b9f5..29be20403d9 100644 --- a/src/components/annotations3d/defaults.js +++ b/src/components/annotations3d/defaults.js @@ -22,7 +22,7 @@ module.exports = function handleDefaults(sceneLayoutIn, sceneLayoutOut, opts) { }); }; -function handleAnnotationDefaults(annIn, annOut, sceneLayout, opts, itemOpts) { +function handleAnnotationDefaults(annIn, annOut, sceneLayout, opts) { function coerce(attr, dflt) { return Lib.coerce(annIn, annOut, attributes, attr, dflt); } @@ -38,8 +38,8 @@ function handleAnnotationDefaults(annIn, annOut, sceneLayout, opts, itemOpts) { } - var visible = coerce('visible', !itemOpts.itemIsNotPlainObject); - if(!visible) return annOut; + var visible = coerce('visible'); + if(!visible) return; handleAnnotationCommonDefaults(annIn, annOut, opts.fullLayout, coerce); @@ -71,6 +71,4 @@ function handleAnnotationDefaults(annIn, annOut, sceneLayout, opts, itemOpts) { // if you have one part of arrow length you should have both Lib.noneOrAll(annIn, annOut, ['ax', 'ay']); } - - return annOut; } diff --git a/src/components/rangeselector/button_attributes.js b/src/components/rangeselector/button_attributes.js index fff7645e2d5..2551a671150 100644 --- a/src/components/rangeselector/button_attributes.js +++ b/src/components/rangeselector/button_attributes.js @@ -13,6 +13,7 @@ module.exports = { visible: { valType: 'boolean', role: 'info', + dflt: true, editType: 'plot', description: 'Determines whether or not this button is visible.' }, diff --git a/src/components/rangeselector/defaults.js b/src/components/rangeselector/defaults.js index 0110ec05ae6..d458acc3631 100644 --- a/src/components/rangeselector/defaults.js +++ b/src/components/rangeselector/defaults.js @@ -33,32 +33,32 @@ module.exports = function handleDefaults(containerIn, containerOut, layout, coun }); var visible = coerce('visible', buttons.length > 0); - if(!visible) return; - - var posDflt = getPosDflt(containerOut, layout, counterAxes); - coerce('x', posDflt[0]); - coerce('y', posDflt[1]); - Lib.noneOrAll(containerIn, containerOut, ['x', 'y']); + if(visible) { + var posDflt = getPosDflt(containerOut, layout, counterAxes); + coerce('x', posDflt[0]); + coerce('y', posDflt[1]); + Lib.noneOrAll(containerIn, containerOut, ['x', 'y']); - coerce('xanchor'); - coerce('yanchor'); + coerce('xanchor'); + coerce('yanchor'); - Lib.coerceFont(coerce, 'font', layout.font); + Lib.coerceFont(coerce, 'font', layout.font); - var bgColor = coerce('bgcolor'); - coerce('activecolor', Color.contrast(bgColor, constants.lightAmount, constants.darkAmount)); - coerce('bordercolor'); - coerce('borderwidth'); + var bgColor = coerce('bgcolor'); + coerce('activecolor', Color.contrast(bgColor, constants.lightAmount, constants.darkAmount)); + coerce('bordercolor'); + coerce('borderwidth'); + } }; -function buttonDefaults(buttonIn, buttonOut, selectorOut, opts, itemOpts) { +function buttonDefaults(buttonIn, buttonOut, selectorOut, opts) { var calendar = opts.calendar; function coerce(attr, dflt) { return Lib.coerce(buttonIn, buttonOut, buttonAttrs, attr, dflt); } - var visible = coerce('visible', !itemOpts.itemIsNotPlainObject); + var visible = coerce('visible'); if(visible) { var step = coerce('step'); diff --git a/src/components/shapes/defaults.js b/src/components/shapes/defaults.js index f685a3eccc5..bc2934e0fee 100644 --- a/src/components/shapes/defaults.js +++ b/src/components/shapes/defaults.js @@ -24,14 +24,14 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut) { }); }; -function handleShapeDefaults(shapeIn, shapeOut, fullLayout, opts, itemOpts) { +function handleShapeDefaults(shapeIn, shapeOut, fullLayout) { function coerce(attr, dflt) { return Lib.coerce(shapeIn, shapeOut, attributes, attr, dflt); } - var visible = coerce('visible', !itemOpts.itemIsNotPlainObject); + var visible = coerce('visible'); - if(!visible) return shapeOut; + if(!visible) return; coerce('layer'); coerce('opacity'); @@ -119,6 +119,4 @@ function handleShapeDefaults(shapeIn, shapeOut, fullLayout, opts, itemOpts) { else { Lib.noneOrAll(shapeIn, shapeOut, ['x0', 'x1', 'y0', 'y1']); } - - return shapeOut; } diff --git a/src/components/sliders/defaults.js b/src/components/sliders/defaults.js index af3faad03fd..b2112385375 100644 --- a/src/components/sliders/defaults.js +++ b/src/components/sliders/defaults.js @@ -100,15 +100,16 @@ function stepDefaults(valueIn, valueOut, sliderOut, opts, itemOpts) { } var visible; - if(itemOpts.itemIsNotPlainObject || (valueIn.method !== 'skip' && !Array.isArray(valueIn.args))) { + if(valueIn.method !== 'skip' && !Array.isArray(valueIn.args)) { visible = valueOut.visible = false; } else visible = coerce('visible'); - if(!visible) return; - coerce('method'); - coerce('args'); - var label = coerce('label', 'step-' + itemOpts.index); - coerce('value', label); - coerce('execute'); + if(visible) { + coerce('method'); + coerce('args'); + var label = coerce('label', 'step-' + itemOpts.index); + coerce('value', label); + coerce('execute'); + } } diff --git a/src/components/updatemenus/defaults.js b/src/components/updatemenus/defaults.js index 96fd5e0dc54..1cce74a2c96 100644 --- a/src/components/updatemenus/defaults.js +++ b/src/components/updatemenus/defaults.js @@ -65,12 +65,12 @@ function menuDefaults(menuIn, menuOut, layoutOut) { coerce('borderwidth'); } -function buttonDefaults(buttonIn, buttonOut, selectorOut, opts, itemOpts) { +function buttonDefaults(buttonIn, buttonOut) { function coerce(attr, dflt) { return Lib.coerce(buttonIn, buttonOut, buttonAttrs, attr, dflt); } - var visible = coerce('visible', !itemOpts.itemIsNotPlainObject && + var visible = coerce('visible', (buttonIn.method === 'skip' || Array.isArray(buttonIn.args))); if(visible) { coerce('method'); diff --git a/src/plots/array_container_defaults.js b/src/plots/array_container_defaults.js index b7e20ecb1f7..65549012831 100644 --- a/src/plots/array_container_defaults.js +++ b/src/plots/array_container_defaults.js @@ -34,7 +34,6 @@ var Template = require('../plot_api/plot_template'); * - parentObj {object} (as in closure) * - opts {object} (as in closure) * - itemOpts {object} - * - itemIsNotPlainObject {boolean} * - index {integer} * N.B. * @@ -57,13 +56,15 @@ module.exports = function handleArrayContainerDefaults(parentObjIn, parentObjOut var itemOpts = {}; if(!Lib.isPlainObject(itemIn)) { - itemOpts.itemIsNotPlainObject = true; - itemIn = {}; + itemOut = templater.newItem({}); + itemOut.visible = false; } - itemOut = templater.newItem(itemIn); + else { + itemOut = templater.newItem(itemIn); - if(itemOut.visible !== false) { - opts.handleItemDefaults(itemIn, itemOut, parentObjOut, opts, itemOpts); + if(itemOut.visible !== false) { + opts.handleItemDefaults(itemIn, itemOut, parentObjOut, opts, itemOpts); + } } itemOut._input = itemIn; diff --git a/src/plots/cartesian/tick_label_defaults.js b/src/plots/cartesian/tick_label_defaults.js index 62e594ee2c5..caf0321a116 100644 --- a/src/plots/cartesian/tick_label_defaults.js +++ b/src/plots/cartesian/tick_label_defaults.js @@ -84,12 +84,12 @@ function getShowAttrDflt(containerIn) { } } -function tickformatstopDefaults(valueIn, valueOut, contOut, opts, itemOpts) { +function tickformatstopDefaults(valueIn, valueOut) { function coerce(attr, dflt) { return Lib.coerce(valueIn, valueOut, layoutAttributes.tickformatstops, attr, dflt); } - var visible = coerce('visible', !itemOpts.itemIsNotPlainObject); + var visible = coerce('visible'); if(visible) { coerce('dtickrange'); coerce('value'); diff --git a/src/plots/mapbox/layout_defaults.js b/src/plots/mapbox/layout_defaults.js index b3584239b47..377dbea7281 100644 --- a/src/plots/mapbox/layout_defaults.js +++ b/src/plots/mapbox/layout_defaults.js @@ -44,12 +44,12 @@ function handleDefaults(containerIn, containerOut, coerce, opts) { containerOut._input = containerIn; } -function handleLayerDefaults(layerIn, layerOut, mapboxOut, opts, itemOpts) { +function handleLayerDefaults(layerIn, layerOut) { function coerce(attr, dflt) { return Lib.coerce(layerIn, layerOut, layoutAttributes.layers, attr, dflt); } - var visible = coerce('visible', !itemOpts.itemIsNotPlainObject); + var visible = coerce('visible'); if(visible) { var sourceType = coerce('sourcetype'); coerce('source'); diff --git a/src/traces/parcoords/defaults.js b/src/traces/parcoords/defaults.js index 3ce8cf0a168..e88b9f0b188 100644 --- a/src/traces/parcoords/defaults.js +++ b/src/traces/parcoords/defaults.js @@ -38,14 +38,14 @@ function handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce) { return Infinity; } -function dimensionDefaults(dimensionIn, dimensionOut, traceOut, opts, itemOpts) { +function dimensionDefaults(dimensionIn, dimensionOut) { function coerce(attr, dflt) { return Lib.coerce(dimensionIn, dimensionOut, attributes.dimensions, attr, dflt); } var values = coerce('values'); var visible = coerce('visible'); - if(!(values && values.length && !itemOpts.itemIsNotPlainObject)) { + if(!(values && values.length)) { visible = dimensionOut.visible = false; } diff --git a/src/traces/splom/defaults.js b/src/traces/splom/defaults.js index 72665747321..afcb002e8b3 100644 --- a/src/traces/splom/defaults.js +++ b/src/traces/splom/defaults.js @@ -51,18 +51,16 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout Lib.coerceSelectionMarkerOpacity(traceOut, coerce); }; -function dimensionDefaults(dimIn, dimOut, traceOut, opts, itemOpts) { +function dimensionDefaults(dimIn, dimOut) { function coerce(attr, dflt) { return Lib.coerce(dimIn, dimOut, attributes.dimensions, attr, dflt); } coerce('label'); - coerce('visible'); var values = coerce('values'); - if(!(values && values.length && !itemOpts.itemIsNotPlainObject)) { - dimOut.visible = false; - } + if(!(values && values.length)) dimOut.visible = false; + else coerce('visible'); } function handleAxisDefaults(traceIn, traceOut, layout, coerce) { diff --git a/test/jasmine/tests/annotations_test.js b/test/jasmine/tests/annotations_test.js index 7054a792a92..63109e4f94c 100644 --- a/test/jasmine/tests/annotations_test.js +++ b/test/jasmine/tests/annotations_test.js @@ -57,9 +57,7 @@ describe('Test annotations', function() { out.forEach(function(item, i) { expect(item).toEqual(jasmine.objectContaining({ visible: false, - _input: {}, - _index: i, - clicktoshow: false + _index: i })); }); }); diff --git a/test/jasmine/tests/shapes_test.js b/test/jasmine/tests/shapes_test.js index 4a0cd164b25..6e6764df1be 100644 --- a/test/jasmine/tests/shapes_test.js +++ b/test/jasmine/tests/shapes_test.js @@ -79,7 +79,6 @@ describe('Test shapes defaults:', function() { out.forEach(function(item, i) { expect(item).toEqual(jasmine.objectContaining({ visible: false, - _input: {}, _index: i })); }); diff --git a/test/jasmine/tests/updatemenus_test.js b/test/jasmine/tests/updatemenus_test.js index 205eae923f1..af376c3eafb 100644 --- a/test/jasmine/tests/updatemenus_test.js +++ b/test/jasmine/tests/updatemenus_test.js @@ -51,8 +51,6 @@ describe('update menus defaults', function() { layoutOut.updatemenus.forEach(function(item, i) { expect(item).toEqual(jasmine.objectContaining({ visible: false, - buttons: [], - _input: {}, _index: i })); }); From 8525953cc080cecc9f2f1a948c70c88a6275a098 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 26 Jun 2018 18:05:20 -0400 Subject: [PATCH 09/24] add template attributes to array containers --- src/components/annotations/attributes.js | 7 +- src/components/annotations3d/attributes.js | 7 +- src/components/images/attributes.js | 7 +- src/components/rangeselector/attributes.js | 62 +++++++++++++++-- .../rangeselector/button_attributes.js | 68 ------------------- src/components/rangeselector/defaults.js | 3 +- src/components/shapes/attributes.js | 7 +- src/components/sliders/attributes.js | 13 ++-- src/components/updatemenus/attributes.js | 12 ++-- src/plot_api/plot_template.js | 51 +++++++++++++- src/plots/cartesian/layout_attributes.js | 7 +- src/plots/mapbox/layout_attributes.js | 7 +- src/traces/parcoords/attributes.js | 6 +- src/traces/splom/attributes.js | 7 +- 14 files changed, 142 insertions(+), 122 deletions(-) delete mode 100644 src/components/rangeselector/button_attributes.js diff --git a/src/components/annotations/attributes.js b/src/components/annotations/attributes.js index 8559c240f68..be7189e2e31 100644 --- a/src/components/annotations/attributes.js +++ b/src/components/annotations/attributes.js @@ -11,11 +11,10 @@ var ARROWPATHS = require('./arrow_paths'); var fontAttrs = require('../../plots/font_attributes'); var cartesianConstants = require('../../plots/cartesian/constants'); +var templatedArray = require('../../plot_api/plot_template').templatedArray; -module.exports = { - _isLinkedToArray: 'annotation', - +module.exports = templatedArray('annotation', { visible: { valType: 'boolean', role: 'info', @@ -543,4 +542,4 @@ module.exports = { ].join(' ') } } -}; +}); diff --git a/src/components/annotations3d/attributes.js b/src/components/annotations3d/attributes.js index ccf3b396a0d..f197160ba2a 100644 --- a/src/components/annotations3d/attributes.js +++ b/src/components/annotations3d/attributes.js @@ -11,10 +11,9 @@ var annAtts = require('../annotations/attributes'); var overrideAll = require('../../plot_api/edit_types').overrideAll; +var templatedArray = require('../../plot_api/plot_template').templatedArray; -module.exports = overrideAll({ - _isLinkedToArray: 'annotation', - +module.exports = overrideAll(templatedArray('annotation', { visible: annAtts.visible, x: { valType: 'any', @@ -94,4 +93,4 @@ module.exports = overrideAll({ // xref: 'x' // yref: 'y // zref: 'z' -}, 'calc', 'from-root'); +}), 'calc', 'from-root'); diff --git a/src/components/images/attributes.js b/src/components/images/attributes.js index 079be6b753e..aef1d2806b7 100644 --- a/src/components/images/attributes.js +++ b/src/components/images/attributes.js @@ -9,11 +9,10 @@ 'use strict'; var cartesianConstants = require('../../plots/cartesian/constants'); +var templatedArray = require('../../plot_api/plot_template').templatedArray; -module.exports = { - _isLinkedToArray: 'image', - +module.exports = templatedArray('image', { visible: { valType: 'boolean', role: 'info', @@ -178,4 +177,4 @@ module.exports = { ].join(' ') }, editType: 'arraydraw' -}; +}); diff --git a/src/components/rangeselector/attributes.js b/src/components/rangeselector/attributes.js index 68c80e00e88..52fd1dea47e 100644 --- a/src/components/rangeselector/attributes.js +++ b/src/components/rangeselector/attributes.js @@ -10,12 +10,64 @@ var fontAttrs = require('../../plots/font_attributes'); var colorAttrs = require('../color/attributes'); -var extendFlat = require('../../lib/extend').extendFlat; -var buttonAttrs = require('./button_attributes'); - -buttonAttrs = extendFlat(buttonAttrs, { - _isLinkedToArray: 'button', +var templatedArray = require('../../plot_api/plot_template').templatedArray; +var buttonAttrs = templatedArray('button', { + visible: { + valType: 'boolean', + role: 'info', + dflt: true, + editType: 'plot', + description: 'Determines whether or not this button is visible.' + }, + step: { + valType: 'enumerated', + role: 'info', + values: ['month', 'year', 'day', 'hour', 'minute', 'second', 'all'], + dflt: 'month', + editType: 'plot', + description: [ + 'The unit of measurement that the `count` value will set the range by.' + ].join(' ') + }, + stepmode: { + valType: 'enumerated', + role: 'info', + values: ['backward', 'todate'], + dflt: 'backward', + editType: 'plot', + description: [ + 'Sets the range update mode.', + 'If *backward*, the range update shifts the start of range', + 'back *count* times *step* milliseconds.', + 'If *todate*, the range update shifts the start of range', + 'back to the first timestamp from *count* times', + '*step* milliseconds back.', + 'For example, with `step` set to *year* and `count` set to *1*', + 'the range update shifts the start of the range back to', + 'January 01 of the current year.', + 'Month and year *todate* are currently available only', + 'for the built-in (Gregorian) calendar.' + ].join(' ') + }, + count: { + valType: 'number', + role: 'info', + min: 0, + dflt: 1, + editType: 'plot', + description: [ + 'Sets the number of steps to take to update the range.', + 'Use with `step` to specify the update interval.' + ].join(' ') + }, + label: { + valType: 'string', + role: 'info', + editType: 'plot', + description: 'Sets the text label to appear on the button.' + }, + editType: 'plot', description: [ 'Sets the specifications for each buttons.', 'By default, a range selector comes with no buttons.' diff --git a/src/components/rangeselector/button_attributes.js b/src/components/rangeselector/button_attributes.js deleted file mode 100644 index 2551a671150..00000000000 --- a/src/components/rangeselector/button_attributes.js +++ /dev/null @@ -1,68 +0,0 @@ -/** -* Copyright 2012-2018, 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'; - - -module.exports = { - visible: { - valType: 'boolean', - role: 'info', - dflt: true, - editType: 'plot', - description: 'Determines whether or not this button is visible.' - }, - step: { - valType: 'enumerated', - role: 'info', - values: ['month', 'year', 'day', 'hour', 'minute', 'second', 'all'], - dflt: 'month', - editType: 'plot', - description: [ - 'The unit of measurement that the `count` value will set the range by.' - ].join(' ') - }, - stepmode: { - valType: 'enumerated', - role: 'info', - values: ['backward', 'todate'], - dflt: 'backward', - editType: 'plot', - description: [ - 'Sets the range update mode.', - 'If *backward*, the range update shifts the start of range', - 'back *count* times *step* milliseconds.', - 'If *todate*, the range update shifts the start of range', - 'back to the first timestamp from *count* times', - '*step* milliseconds back.', - 'For example, with `step` set to *year* and `count` set to *1*', - 'the range update shifts the start of the range back to', - 'January 01 of the current year.', - 'Month and year *todate* are currently available only', - 'for the built-in (Gregorian) calendar.' - ].join(' ') - }, - count: { - valType: 'number', - role: 'info', - min: 0, - dflt: 1, - editType: 'plot', - description: [ - 'Sets the number of steps to take to update the range.', - 'Use with `step` to specify the update interval.' - ].join(' ') - }, - label: { - valType: 'string', - role: 'info', - editType: 'plot', - description: 'Sets the text label to appear on the button.' - }, - editType: 'plot' -}; diff --git a/src/components/rangeselector/defaults.js b/src/components/rangeselector/defaults.js index d458acc3631..0d5e95f64a7 100644 --- a/src/components/rangeselector/defaults.js +++ b/src/components/rangeselector/defaults.js @@ -14,7 +14,6 @@ var Template = require('../../plot_api/plot_template'); var handleArrayContainerDefaults = require('../../plots/array_container_defaults'); var attributes = require('./attributes'); -var buttonAttrs = require('./button_attributes'); var constants = require('./constants'); @@ -55,7 +54,7 @@ function buttonDefaults(buttonIn, buttonOut, selectorOut, opts) { var calendar = opts.calendar; function coerce(attr, dflt) { - return Lib.coerce(buttonIn, buttonOut, buttonAttrs, attr, dflt); + return Lib.coerce(buttonIn, buttonOut, attributes.buttons, attr, dflt); } var visible = coerce('visible'); diff --git a/src/components/shapes/attributes.js b/src/components/shapes/attributes.js index 7fce0cce265..0a4e5d45891 100644 --- a/src/components/shapes/attributes.js +++ b/src/components/shapes/attributes.js @@ -12,10 +12,9 @@ var annAttrs = require('../annotations/attributes'); var scatterLineAttrs = require('../../traces/scatter/attributes').line; var dash = require('../drawing/attributes').dash; var extendFlat = require('../../lib/extend').extendFlat; +var templatedArray = require('../../plot_api/plot_template').templatedArray; -module.exports = { - _isLinkedToArray: 'shape', - +module.exports = templatedArray('shape', { visible: { valType: 'boolean', role: 'info', @@ -240,4 +239,4 @@ module.exports = { ].join(' ') }, editType: 'arraydraw' -}; +}); diff --git a/src/components/sliders/attributes.js b/src/components/sliders/attributes.js index 1ab099589c3..574dee98e9d 100644 --- a/src/components/sliders/attributes.js +++ b/src/components/sliders/attributes.js @@ -13,11 +13,10 @@ var padAttrs = require('../../plots/pad_attributes'); var extendDeepAll = require('../../lib/extend').extendDeepAll; var overrideAll = require('../../plot_api/edit_types').overrideAll; var animationAttrs = require('../../plots/animation_attributes'); +var templatedArray = require('../../plot_api/plot_template').templatedArray; var constants = require('./constants'); -var stepsAttrs = { - _isLinkedToArray: 'step', - +var stepsAttrs = templatedArray('step', { visible: { valType: 'boolean', role: 'info', @@ -78,11 +77,9 @@ var stepsAttrs = { 'specification of `method` and `args`.' ].join(' ') } -}; - -module.exports = overrideAll({ - _isLinkedToArray: 'slider', +}); +module.exports = overrideAll(templatedArray('slider', { visible: { valType: 'boolean', role: 'info', @@ -293,4 +290,4 @@ module.exports = overrideAll({ role: 'style', description: 'Sets the length in pixels of minor step tick marks' } -}, 'arraydraw', 'from-root'); +}), 'arraydraw', 'from-root'); diff --git a/src/components/updatemenus/attributes.js b/src/components/updatemenus/attributes.js index c64149b54a7..6224392ed02 100644 --- a/src/components/updatemenus/attributes.js +++ b/src/components/updatemenus/attributes.js @@ -13,10 +13,9 @@ var colorAttrs = require('../color/attributes'); var extendFlat = require('../../lib/extend').extendFlat; var overrideAll = require('../../plot_api/edit_types').overrideAll; var padAttrs = require('../../plots/pad_attributes'); +var templatedArray = require('../../plot_api/plot_template').templatedArray; -var buttonsAttrs = { - _isLinkedToArray: 'button', - +var buttonsAttrs = templatedArray('button', { visible: { valType: 'boolean', role: 'info', @@ -67,10 +66,9 @@ var buttonsAttrs = { 'specification of `method` and `args`.' ].join(' ') } -}; +}); -module.exports = overrideAll({ - _isLinkedToArray: 'updatemenu', +module.exports = overrideAll(templatedArray('updatemenu', { _arrayAttrRegexps: [/^updatemenus\[(0|[1-9][0-9]+)\]\.buttons/], visible: { @@ -191,4 +189,4 @@ module.exports = overrideAll({ editType: 'arraydraw', description: 'Sets the width (in px) of the border enclosing the update menu.' } -}, 'arraydraw', 'from-root'); +}), 'arraydraw', 'from-root'); diff --git a/src/plot_api/plot_template.js b/src/plot_api/plot_template.js index 5821060e6a0..8030f6fca7e 100644 --- a/src/plot_api/plot_template.js +++ b/src/plot_api/plot_template.js @@ -12,7 +12,56 @@ var Lib = require('../lib'); var plotAttributes = require('../plots/attributes'); -var TEMPLATEITEMNAME = exports.TEMPLATEITEMNAME = 'templateitemname'; +var TEMPLATEITEMNAME = 'templateitemname'; + +var templateAttrs = { + name: { + valType: 'string', + role: 'style', + editType: 'none', + description: [ + 'When used in a template, named items are created in the output figure', + 'in addition to any items the figure already has in this array.', + 'You can modify these items in the output figure by making your own', + 'item with `templateitemname` matching this `name`', + 'alongside your modifications (including `visible: false` to hide it).', + 'Has no effect outside of a template.' + ].join(' ') + } +}; +templateAttrs[TEMPLATEITEMNAME] = { + valType: 'string', + role: 'info', + editType: 'calc', + description: [ + 'Used to refer to a named item in this array in the template. Named', + 'items from the template will be created even without a matching item', + 'in the input figure, but you can modify one by making an item with', + '`templateitemname` matching its `name`, alongside your', + 'modifications (including `visible: false` to hide it).', + 'If there is no template or no matching item, this item will be', + 'hidden unless you explicitly show it with `visible: true`.' + ].join(' ') +}; + +/** + * templatedArray: decorate an attributes object with templating (and array) + * properties. + * + * @param {string} name: the singular form of the array name. Sets + * `_isLinkedToArray` to this, so the schema knows to treat this as an array. + * @param {object} attrs: the item attributes. Since all callers are expected + * to be constructing this object on the spot, we mutate it here for + * performance, rather than extending a new object with it. + * + * @returns {object}: the decorated `attrs` object + */ +exports.templatedArray = function(name, attrs) { + attrs._isLinkedToArray = name; + attrs.name = templateAttrs.name; + attrs[TEMPLATEITEMNAME] = templateAttrs[TEMPLATEITEMNAME]; + return attrs; +}; /** * traceTemplater: logic for matching traces to trace templates diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 9c3c7de77b0..e701740a86e 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -12,6 +12,7 @@ var fontAttrs = require('../font_attributes'); var colorAttrs = require('../../components/color/attributes'); var dash = require('../../components/drawing/attributes').dash; var extendFlat = require('../../lib/extend').extendFlat; +var templatedArray = require('../../plot_api/plot_template').templatedArray; var constants = require('./constants'); @@ -510,9 +511,7 @@ module.exports = { '*%H~%M~%S.%2f* would display *09~15~23.46*' ].join(' ') }, - tickformatstops: { - _isLinkedToArray: 'tickformatstop', - + tickformatstops: templatedArray('tickformatstop', { visible: { valType: 'boolean', role: 'info', @@ -547,7 +546,7 @@ module.exports = { ].join(' ') }, editType: 'ticks' - }, + }), hoverformat: { valType: 'string', dflt: '', diff --git a/src/plots/mapbox/layout_attributes.js b/src/plots/mapbox/layout_attributes.js index 59f9f3006d1..4d59225f208 100644 --- a/src/plots/mapbox/layout_attributes.js +++ b/src/plots/mapbox/layout_attributes.js @@ -15,6 +15,7 @@ var domainAttrs = require('../domain').attributes; var fontAttrs = require('../font_attributes'); var textposition = require('../../traces/scatter/attributes').textposition; var overrideAll = require('../../plot_api/edit_types').overrideAll; +var templatedArray = require('../../plot_api/plot_template').templatedArray; var fontAttr = fontAttrs({ description: [ @@ -88,9 +89,7 @@ module.exports = overrideAll({ ].join(' ') }, - layers: { - _isLinkedToArray: 'layer', - + layers: templatedArray('layer', { visible: { valType: 'boolean', role: 'info', @@ -244,5 +243,5 @@ module.exports = overrideAll({ textfont: fontAttr, textposition: Lib.extendFlat({}, textposition, { arrayOk: false }) } - } + }) }, 'plot', 'from-root'); diff --git a/src/traces/parcoords/attributes.js b/src/traces/parcoords/attributes.js index 2fba9d62898..7c0359adb17 100644 --- a/src/traces/parcoords/attributes.js +++ b/src/traces/parcoords/attributes.js @@ -18,6 +18,7 @@ var domainAttrs = require('../../plots/domain').attributes; var extend = require('../../lib/extend'); var extendDeepAll = extend.extendDeepAll; var extendFlat = extend.extendFlat; +var templatedArray = require('../../plot_api/plot_template').templatedArray; module.exports = { domain: domainAttrs({name: 'parcoords', trace: true, editType: 'calc'}), @@ -35,8 +36,7 @@ module.exports = { description: 'Sets the font for the `dimension` range values.' }), - dimensions: { - _isLinkedToArray: 'dimension', + dimensions: templatedArray('dimension', { label: { valType: 'string', role: 'info', @@ -113,7 +113,7 @@ module.exports = { }, editType: 'calc', description: 'The dimensions (variables) of the parallel coordinates chart. 2..60 dimensions are supported.' - }, + }), line: extendFlat( // the default autocolorscale isn't quite usable for parcoords due to context ambiguity around 0 (grey, off-white) diff --git a/src/traces/splom/attributes.js b/src/traces/splom/attributes.js index aa73330548d..0c5dd6a9185 100644 --- a/src/traces/splom/attributes.js +++ b/src/traces/splom/attributes.js @@ -10,6 +10,7 @@ var scatterGlAttrs = require('../scattergl/attributes'); var cartesianIdRegex = require('../../plots/cartesian/constants').idRegex; +var templatedArray = require('../../plot_api/plot_template').templatedArray; function makeAxesValObject(axLetter) { return { @@ -32,9 +33,7 @@ function makeAxesValObject(axLetter) { } module.exports = { - dimensions: { - _isLinkedToArray: 'dimension', - + dimensions: templatedArray('dimension', { visible: { valType: 'boolean', role: 'info', @@ -66,7 +65,7 @@ module.exports = { // maybe more axis defaulting option e.g. `showgrid: false` editType: 'calc+clearAxisTypes' - }, + }), // mode: {}, (only 'markers' for now) From e306d1c00fa196119933aaee4e505465de8a77d8 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 26 Jun 2018 23:40:27 -0400 Subject: [PATCH 10/24] template-safe axis default color inheritance logic there may be other similar logic we need to update for templates, but these are the most readily apparent ones --- src/plots/cartesian/axis_defaults.js | 4 +++- src/plots/cartesian/tick_label_defaults.js | 2 +- src/plots/ternary/layout/axis_defaults.js | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 220e5eca798..bdd58de1af7 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -69,7 +69,9 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, var dfltColor = coerce('color'); // if axis.color was provided, use it for fonts too; otherwise, // inherit from global font color in case that was provided. - var dfltFontColor = (dfltColor === containerIn.color) ? dfltColor : font.color; + // Compare to dflt rather than to containerIn, so we can provide color via + // template too. + var dfltFontColor = (dfltColor !== layoutAttributes.color.dflt) ? dfltColor : font.color; // try to get default title from splom trace, fallback to graph-wide value var dfltTitle = ((layoutOut._splomAxes || {})[letter] || {})[id] || layoutOut._dfltTitle[letter]; diff --git a/src/plots/cartesian/tick_label_defaults.js b/src/plots/cartesian/tick_label_defaults.js index caf0321a116..8825ba7d423 100644 --- a/src/plots/cartesian/tick_label_defaults.js +++ b/src/plots/cartesian/tick_label_defaults.js @@ -27,7 +27,7 @@ module.exports = function handleTickLabelDefaults(containerIn, containerOut, coe var font = options.font || {}; // as with titlefont.color, inherit axis.color only if one was // explicitly provided - var dfltFontColor = (containerOut.color === containerIn.color) ? + var dfltFontColor = (containerOut.color !== layoutAttributes.color.dflt) ? containerOut.color : font.color; Lib.coerceFont(coerce, 'tickfont', { family: font.family, diff --git a/src/plots/ternary/layout/axis_defaults.js b/src/plots/ternary/layout/axis_defaults.js index f4b1e8d5c0b..aa4f984c22a 100644 --- a/src/plots/ternary/layout/axis_defaults.js +++ b/src/plots/ternary/layout/axis_defaults.js @@ -25,7 +25,7 @@ module.exports = function supplyLayoutDefaults(containerIn, containerOut, option var dfltColor = coerce('color'); // if axis.color was provided, use it for fonts too; otherwise, // inherit from global font color in case that was provided. - var dfltFontColor = (dfltColor === containerIn.color) ? dfltColor : options.font.color; + var dfltFontColor = (dfltColor !== layoutAttributes.color.dflt) ? dfltColor : options.font.color; var axName = containerOut._name, letterUpper = axName.charAt(0).toUpperCase(), From 8e2a3210adb53679f095585c06ef705ab42c30b6 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 27 Jun 2018 16:28:12 -0400 Subject: [PATCH 11/24] template-safe GUI editing of array objects --- src/components/annotations/click.js | 21 +++-- src/components/annotations/draw.js | 75 +++++++++--------- src/components/shapes/draw.js | 108 +++++++++++++------------- src/components/sliders/draw.js | 7 +- src/components/updatemenus/draw.js | 7 +- src/plot_api/plot_template.js | 105 +++++++++++++++++++++++-- src/plots/array_container_defaults.js | 13 ++-- 7 files changed, 220 insertions(+), 116 deletions(-) diff --git a/src/components/annotations/click.js b/src/components/annotations/click.js index b2f300fb9be..e9bb11aae89 100644 --- a/src/components/annotations/click.js +++ b/src/components/annotations/click.js @@ -8,7 +8,9 @@ 'use strict'; +var Lib = require('../../lib'); var Registry = require('../../registry'); +var arrayEditor = require('../../plot_api/plot_template').arrayEditor; module.exports = { hasClickToShow: hasClickToShow, @@ -41,20 +43,25 @@ function hasClickToShow(gd, hoverData) { * returns: Promise that the update is complete */ function onClick(gd, hoverData) { - var toggleSets = getToggleSets(gd, hoverData), - onSet = toggleSets.on, - offSet = toggleSets.off.concat(toggleSets.explicitOff), - update = {}, - i; + var toggleSets = getToggleSets(gd, hoverData); + var onSet = toggleSets.on; + var offSet = toggleSets.off.concat(toggleSets.explicitOff); + var update = {}; + var annotationsOut = gd._fullLayout.annotations; + var i, editHelpers; if(!(onSet.length || offSet.length)) return; for(i = 0; i < onSet.length; i++) { - update['annotations[' + onSet[i] + '].visible'] = true; + editHelpers = arrayEditor(gd.layout, 'annotations', annotationsOut[onSet[i]]); + editHelpers.modifyItem('visible', true); + Lib.extendFlat(update, editHelpers.getUpdateObj()); } for(i = 0; i < offSet.length; i++) { - update['annotations[' + offSet[i] + '].visible'] = false; + editHelpers = arrayEditor(gd.layout, 'annotations', annotationsOut[offSet[i]]); + editHelpers.modifyItem('visible', false); + Lib.extendFlat(update, editHelpers.getUpdateObj()); } return Registry.call('update', gd, {}, update); diff --git a/src/components/annotations/draw.js b/src/components/annotations/draw.js index 3dd8bd166ce..f3500145a01 100644 --- a/src/components/annotations/draw.js +++ b/src/components/annotations/draw.js @@ -20,6 +20,8 @@ var Fx = require('../fx'); var svgTextUtils = require('../../lib/svg_text_utils'); var setCursor = require('../../lib/setcursor'); var dragElement = require('../dragelement'); +var arrayEditor = require('../../plot_api/plot_template').arrayEditor; + var drawArrowHead = require('./draw_arrow_head'); // Annotations are stored in gd.layout.annotations, an array of objects @@ -84,17 +86,21 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { var gs = gd._fullLayout._size; var edits = gd._context.edits; - var className; - var annbase; + var className, containerStr; if(subplotId) { className = 'annotation-' + subplotId; - annbase = subplotId + '.annotations[' + index + ']'; + containerStr = subplotId + '.annotations'; } else { className = 'annotation'; - annbase = 'annotations[' + index + ']'; + containerStr = 'annotations'; } + var editHelpers = arrayEditor(gd.layout, containerStr, options); + var modifyBase = editHelpers.modifyBase; + var modifyItem = editHelpers.modifyItem; + var getUpdateObj = editHelpers.getUpdateObj; + // remove the existing annotation if there is one fullLayout._infolayer .selectAll('.' + className + '[data-index="' + index + '"]') @@ -542,9 +548,7 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { .call(Color.stroke, 'rgba(0,0,0,0)') .call(Color.fill, 'rgba(0,0,0,0)'); - var update, - annx0, - anny0; + var annx0, anny0; // dragger for the arrow & head: translates the whole thing // (head/tail/text) all together @@ -556,12 +560,11 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { annx0 = pos.x; anny0 = pos.y; - update = {}; if(xa && xa.autorange) { - update[xa._name + '.autorange'] = true; + modifyBase(xa._name + '.autorange', true); } if(ya && ya.autorange) { - update[ya._name + '.autorange'] = true; + modifyBase(ya._name + '.autorange', true); } }, moveFn: function(dx, dy) { @@ -570,19 +573,19 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { ycenter = annxy0[1] + dy; annTextGroupInner.call(Drawing.setTranslate, xcenter, ycenter); - update[annbase + '.x'] = xa ? + modifyItem('x', xa ? xa.p2r(xa.r2p(options.x) + dx) : - (options.x + (dx / gs.w)); - update[annbase + '.y'] = ya ? + (options.x + (dx / gs.w))); + modifyItem('y', ya ? ya.p2r(ya.r2p(options.y) + dy) : - (options.y - (dy / gs.h)); + (options.y - (dy / gs.h))); if(options.axref === options.xref) { - update[annbase + '.ax'] = xa.p2r(xa.r2p(options.ax) + dx); + modifyItem('ax', xa.p2r(xa.r2p(options.ax) + dx)); } if(options.ayref === options.yref) { - update[annbase + '.ay'] = ya.p2r(ya.r2p(options.ay) + dy); + modifyItem('ay', ya.p2r(ya.r2p(options.ay) + dy)); } arrowGroup.attr('transform', 'translate(' + dx + ',' + dy + ')'); @@ -592,7 +595,7 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { }); }, doneFn: function() { - Registry.call('relayout', gd, update); + Registry.call('relayout', gd, getUpdateObj()); var notesBox = document.querySelector('.js-notes-box-panel'); if(notesBox) notesBox.redraw(notesBox.selectedObj); } @@ -604,8 +607,7 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { // user dragging the annotation (text, not arrow) if(editTextPosition) { - var update, - baseTextTransform; + var baseTextTransform; // dragger for the textbox: if there's an arrow, just drag the // textbox and tail, leave the head untouched @@ -614,52 +616,54 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { gd: gd, prepFn: function() { baseTextTransform = annTextGroup.attr('transform'); - update = {}; }, moveFn: function(dx, dy) { var csr = 'pointer'; if(options.showarrow) { if(options.axref === options.xref) { - update[annbase + '.ax'] = xa.p2r(xa.r2p(options.ax) + dx); + modifyItem('ax', xa.p2r(xa.r2p(options.ax) + dx)); } else { - update[annbase + '.ax'] = options.ax + dx; + modifyItem('ax', options.ax + dx); } if(options.ayref === options.yref) { - update[annbase + '.ay'] = ya.p2r(ya.r2p(options.ay) + dy); + modifyItem('ay', ya.p2r(ya.r2p(options.ay) + dy)); } else { - update[annbase + '.ay'] = options.ay + dy; + modifyItem('ay', options.ay + dy); } drawArrow(dx, dy); } else if(!subplotId) { + var xUpdate, yUpdate; if(xa) { - update[annbase + '.x'] = xa.p2r(xa.r2p(options.x) + dx); + xUpdate = xa.p2r(xa.r2p(options.x) + dx); } else { var widthFraction = options._xsize / gs.w, xLeft = options.x + (options._xshift - options.xshift) / gs.w - widthFraction / 2; - update[annbase + '.x'] = dragElement.align(xLeft + dx / gs.w, + xUpdate = dragElement.align(xLeft + dx / gs.w, widthFraction, 0, 1, options.xanchor); } if(ya) { - update[annbase + '.y'] = ya.p2r(ya.r2p(options.y) + dy); + yUpdate = ya.p2r(ya.r2p(options.y) + dy); } else { var heightFraction = options._ysize / gs.h, yBottom = options.y - (options._yshift + options.yshift) / gs.h - heightFraction / 2; - update[annbase + '.y'] = dragElement.align(yBottom - dy / gs.h, + yUpdate = dragElement.align(yBottom - dy / gs.h, heightFraction, 0, 1, options.yanchor); } + modifyItem('x', xUpdate); + modifyItem('y', yUpdate); if(!xa || !ya) { csr = dragElement.getCursor( - xa ? 0.5 : update[annbase + '.x'], - ya ? 0.5 : update[annbase + '.y'], + xa ? 0.5 : xUpdate, + ya ? 0.5 : yUpdate, options.xanchor, options.yanchor ); } @@ -674,7 +678,7 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { }, doneFn: function() { setCursor(annTextGroupInner); - Registry.call('relayout', gd, update); + Registry.call('relayout', gd, getUpdateObj()); var notesBox = document.querySelector('.js-notes-box-panel'); if(notesBox) notesBox.redraw(notesBox.selectedObj); } @@ -689,17 +693,16 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { options.text = _text; this.call(textLayout); - var update = {}; - update[annbase + '.text'] = options.text; + modifyItem('text', _text); if(xa && xa.autorange) { - update[xa._name + '.autorange'] = true; + modifyBase(xa._name + '.autorange', true); } if(ya && ya.autorange) { - update[ya._name + '.autorange'] = true; + modifyBase(ya._name + '.autorange', true); } - Registry.call('relayout', gd, update); + Registry.call('relayout', gd, getUpdateObj()); }); } else annText.call(textLayout); diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index fef3eb31931..e52ee675513 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -14,6 +14,7 @@ var Lib = require('../../lib'); var Axes = require('../../plots/cartesian/axes'); var Color = require('../color'); var Drawing = require('../drawing'); +var arrayEditor = require('../../plot_api/plot_template').arrayEditor; var dragElement = require('../dragelement'); var setCursor = require('../../lib/setcursor'); @@ -65,12 +66,11 @@ function drawOne(gd, index) { .selectAll('.shapelayer [data-index="' + index + '"]') .remove(); - var optionsIn = (gd.layout.shapes || [])[index], - options = gd._fullLayout.shapes[index]; + var options = gd._fullLayout.shapes[index]; // 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; + if(!options._input || options.visible === false) return; if(options.layer !== 'below') { drawShape(gd._fullLayout._shapeUpperLayer); @@ -127,18 +127,20 @@ function setClipPath(shapePath, gd, shapeOptions) { } function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer) { - var MINWIDTH = 10, - MINHEIGHT = 10; + var MINWIDTH = 10; + var MINHEIGHT = 10; - var xPixelSized = shapeOptions.xsizemode === 'pixel', - yPixelSized = shapeOptions.ysizemode === 'pixel', - isLine = shapeOptions.type === 'line', - isPath = shapeOptions.type === 'path'; + var xPixelSized = shapeOptions.xsizemode === 'pixel'; + var yPixelSized = shapeOptions.ysizemode === 'pixel'; + var isLine = shapeOptions.type === 'line'; + var isPath = shapeOptions.type === 'path'; - var update; - var x0, y0, x1, y1, xAnchor, yAnchor, astrX0, astrY0, astrX1, astrY1, astrXAnchor, astrYAnchor; - var n0, s0, w0, e0, astrN, astrS, astrW, astrE, optN, optS, optW, optE; - var pathIn, astrPath; + var editHelpers = arrayEditor(gd.layout, 'shapes', shapeOptions); + var modifyItem = editHelpers.modifyItem; + + var x0, y0, x1, y1, xAnchor, yAnchor; + var n0, s0, w0, e0, optN, optS, optW, optE; + var pathIn; // setup conversion functions var xa = Axes.getFromId(gd, shapeOptions.xref), @@ -246,55 +248,51 @@ function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer) { function startDrag(evt) { // setup update strings and initial values - var astr = 'shapes[' + index + ']'; - if(xPixelSized) { xAnchor = x2p(shapeOptions.xanchor); - astrXAnchor = astr + '.xanchor'; } if(yPixelSized) { yAnchor = y2p(shapeOptions.yanchor); - astrYAnchor = astr + '.yanchor'; } if(shapeOptions.type === 'path') { pathIn = shapeOptions.path; - astrPath = astr + '.path'; } else { x0 = xPixelSized ? shapeOptions.x0 : x2p(shapeOptions.x0); y0 = yPixelSized ? shapeOptions.y0 : y2p(shapeOptions.y0); x1 = xPixelSized ? shapeOptions.x1 : x2p(shapeOptions.x1); y1 = yPixelSized ? shapeOptions.y1 : y2p(shapeOptions.y1); - - astrX0 = astr + '.x0'; - astrY0 = astr + '.y0'; - astrX1 = astr + '.x1'; - astrY1 = astr + '.y1'; } if(x0 < x1) { - w0 = x0; astrW = astr + '.x0'; optW = 'x0'; - e0 = x1; astrE = astr + '.x1'; optE = 'x1'; + w0 = x0; + optW = 'x0'; + e0 = x1; + optE = 'x1'; } else { - w0 = x1; astrW = astr + '.x1'; optW = 'x1'; - e0 = x0; astrE = astr + '.x0'; optE = 'x0'; + w0 = x1; + optW = 'x1'; + e0 = x0; + optE = 'x0'; } // For fixed size shapes take opposing direction of y-axis into account. // Hint: For data sized shapes this is done by the y2p function. if((!yPixelSized && y0 < y1) || (yPixelSized && y0 > y1)) { - n0 = y0; astrN = astr + '.y0'; optN = 'y0'; - s0 = y1; astrS = astr + '.y1'; optS = 'y1'; + n0 = y0; + optN = 'y0'; + s0 = y1; + optS = 'y1'; } else { - n0 = y1; astrN = astr + '.y1'; optN = 'y1'; - s0 = y0; astrS = astr + '.y0'; optS = 'y0'; + n0 = y1; + optN = 'y1'; + s0 = y0; + optS = 'y0'; } - update = {}; - // setup dragMode and the corresponding handler updateDragMode(evt); renderVisualCues(shapeLayer, shapeOptions); @@ -308,7 +306,7 @@ function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer) { // Don't rely on clipPath being activated during re-layout setClipPath(shapePath, gd, shapeOptions); - Registry.call('relayout', gd, update); + Registry.call('relayout', gd, editHelpers.getUpdateObj()); } function abortDrag() { @@ -322,35 +320,34 @@ function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer) { moveY = noOp; if(xPixelSized) { - update[astrXAnchor] = shapeOptions.xanchor = p2x(xAnchor + dx); + modifyItem('xanchor', shapeOptions.xanchor = p2x(xAnchor + dx)); } else { moveX = function moveX(x) { return p2x(x2p(x) + dx); }; if(xa && xa.type === 'date') moveX = helpers.encodeDate(moveX); } if(yPixelSized) { - update[astrYAnchor] = shapeOptions.yanchor = p2y(yAnchor + dy); + modifyItem('yanchor', shapeOptions.yanchor = p2y(yAnchor + dy)); } else { moveY = function moveY(y) { return p2y(y2p(y) + dy); }; if(ya && ya.type === 'date') moveY = helpers.encodeDate(moveY); } - shapeOptions.path = movePath(pathIn, moveX, moveY); - update[astrPath] = shapeOptions.path; + modifyItem('path', shapeOptions.path = movePath(pathIn, moveX, moveY)); } else { if(xPixelSized) { - update[astrXAnchor] = shapeOptions.xanchor = p2x(xAnchor + dx); + modifyItem('xanchor', shapeOptions.xanchor = p2x(xAnchor + dx)); } else { - update[astrX0] = shapeOptions.x0 = p2x(x0 + dx); - update[astrX1] = shapeOptions.x1 = p2x(x1 + dx); + modifyItem('x0', shapeOptions.x0 = p2x(x0 + dx)); + modifyItem('x1', shapeOptions.x1 = p2x(x1 + dx)); } if(yPixelSized) { - update[astrYAnchor] = shapeOptions.yanchor = p2y(yAnchor + dy); + modifyItem('yanchor', shapeOptions.yanchor = p2y(yAnchor + dy)); } else { - update[astrY0] = shapeOptions.y0 = p2y(y0 + dy); - update[astrY1] = shapeOptions.y1 = p2y(y1 + dy); + modifyItem('y0', shapeOptions.y0 = p2y(y0 + dy)); + modifyItem('y1', shapeOptions.y1 = p2y(y1 + dy)); } } @@ -366,33 +363,32 @@ function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer) { moveY = noOp; if(xPixelSized) { - update[astrXAnchor] = shapeOptions.xanchor = p2x(xAnchor + dx); + modifyItem('xanchor', shapeOptions.xanchor = p2x(xAnchor + dx)); } else { moveX = function moveX(x) { return p2x(x2p(x) + dx); }; if(xa && xa.type === 'date') moveX = helpers.encodeDate(moveX); } if(yPixelSized) { - update[astrYAnchor] = shapeOptions.yanchor = p2y(yAnchor + dy); + modifyItem('yanchor', shapeOptions.yanchor = p2y(yAnchor + dy)); } else { moveY = function moveY(y) { return p2y(y2p(y) + dy); }; if(ya && ya.type === 'date') moveY = helpers.encodeDate(moveY); } - shapeOptions.path = movePath(pathIn, moveX, moveY); - update[astrPath] = shapeOptions.path; + modifyItem('path', shapeOptions.path = movePath(pathIn, moveX, moveY)); } else if(isLine) { if(dragMode === 'resize-over-start-point') { var newX0 = x0 + dx; var newY0 = yPixelSized ? y0 - dy : y0 + dy; - update[astrX0] = shapeOptions.x0 = xPixelSized ? newX0 : p2x(newX0); - update[astrY0] = shapeOptions.y0 = yPixelSized ? newY0 : p2y(newY0); + modifyItem('x0', shapeOptions.x0 = xPixelSized ? newX0 : p2x(newX0)); + modifyItem('y0', shapeOptions.y0 = yPixelSized ? newY0 : p2y(newY0)); } else if(dragMode === 'resize-over-end-point') { var newX1 = x1 + dx; var newY1 = yPixelSized ? y1 - dy : y1 + dy; - update[astrX1] = shapeOptions.x1 = xPixelSized ? newX1 : p2x(newX1); - update[astrY1] = shapeOptions.y1 = yPixelSized ? newY1 : p2y(newY1); + modifyItem('x1', shapeOptions.x1 = xPixelSized ? newX1 : p2x(newX1)); + modifyItem('y1', shapeOptions.y1 = yPixelSized ? newY1 : p2y(newY1)); } } else { @@ -410,12 +406,12 @@ function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer) { // opposing direction of the y-axis of fixed size shapes. if((!yPixelSized && newS - newN > MINHEIGHT) || (yPixelSized && newN - newS > MINHEIGHT)) { - update[astrN] = shapeOptions[optN] = yPixelSized ? newN : p2y(newN); - update[astrS] = shapeOptions[optS] = yPixelSized ? newS : p2y(newS); + modifyItem(optN, shapeOptions[optN] = yPixelSized ? newN : p2y(newN)); + modifyItem(optS, shapeOptions[optS] = yPixelSized ? newS : p2y(newS)); } if(newE - newW > MINWIDTH) { - update[astrW] = shapeOptions[optW] = xPixelSized ? newW : p2x(newW); - update[astrE] = shapeOptions[optE] = xPixelSized ? newE : p2x(newE); + modifyItem(optW, shapeOptions[optW] = xPixelSized ? newW : p2x(newW)); + modifyItem(optE, shapeOptions[optE] = xPixelSized ? newE : p2x(newE)); } } diff --git a/src/components/sliders/draw.js b/src/components/sliders/draw.js index 48b1518605c..62557f92ded 100644 --- a/src/components/sliders/draw.js +++ b/src/components/sliders/draw.js @@ -16,6 +16,7 @@ var Drawing = require('../drawing'); var Lib = require('../../lib'); var svgTextUtils = require('../../lib/svg_text_utils'); var anchorUtils = require('../legend/anchor_utils'); +var arrayEditor = require('../../plot_api/plot_template').arrayEditor; var constants = require('./constants'); var alignmentConstants = require('../../constants/alignment'); @@ -413,7 +414,11 @@ function handleInput(gd, sliderGroup, sliderOpts, normalizedPosition, doTransiti function setActive(gd, sliderGroup, sliderOpts, index, doCallback, doTransition) { var previousActive = sliderOpts.active; - sliderOpts._input.active = sliderOpts.active = index; + sliderOpts.active = index; + + // due to templating, it's possible this slider doesn't even exist yet + arrayEditor(gd.layout, constants.name, sliderOpts) + .applyUpdate('active', index); var step = sliderOpts.steps[sliderOpts.active]; diff --git a/src/components/updatemenus/draw.js b/src/components/updatemenus/draw.js index 49075d67b97..f1e5a9adaad 100644 --- a/src/components/updatemenus/draw.js +++ b/src/components/updatemenus/draw.js @@ -17,6 +17,7 @@ var Drawing = require('../drawing'); var Lib = require('../../lib'); var svgTextUtils = require('../../lib/svg_text_utils'); var anchorUtils = require('../legend/anchor_utils'); +var arrayEditor = require('../../plot_api/plot_template').arrayEditor; var LINE_SPACING = require('../../constants/alignment').LINE_SPACING; @@ -155,7 +156,11 @@ function isActive(gButton, menuOpts) { function setActive(gd, menuOpts, buttonOpts, gHeader, gButton, scrollBox, buttonIndex, isSilentUpdate) { // update 'active' attribute in menuOpts - menuOpts._input.active = menuOpts.active = buttonIndex; + menuOpts.active = buttonIndex; + + // due to templating, it's possible this slider doesn't even exist yet + arrayEditor(gd.layout, constants.name, menuOpts) + .applyUpdate('active', buttonIndex); if(menuOpts.type === 'buttons') { drawButtons(gd, gHeader, null, null, menuOpts); diff --git a/src/plot_api/plot_template.js b/src/plot_api/plot_template.js index 8030f6fca7e..b05ed72f1a1 100644 --- a/src/plot_api/plot_template.js +++ b/src/plot_api/plot_template.js @@ -164,7 +164,7 @@ exports.arrayTemplater = function(container, name) { templateItems = []; } - var usedIndices = {}; + var usedNames = {}; function newItem(itemIn) { // include name and templateitemname in the output object for ALL @@ -172,11 +172,11 @@ exports.arrayTemplater = function(container, name) { // name and templateitemname, if you're using one template to make // another template. templateitemname would be the name in the original // template, and name is the new "subclassed" item name. - var out = {name: itemIn.name}; + var out = {name: itemIn.name, _input: itemIn}; var templateItemName = out[TEMPLATEITEMNAME] = itemIn[TEMPLATEITEMNAME]; // no itemname: use the default template - if(!templateItemName) { + if(!validItemName(templateItemName)) { out._template = defaultsTemplate; return out; } @@ -189,7 +189,7 @@ exports.arrayTemplater = function(container, name) { // Note: it's OK to use a template item more than once // but using it at least once will stop it from generating // a default item at the end. - usedIndices[i] = 1; + usedNames[templateItemName] = 1; out._template = templateItem; return out; } @@ -206,11 +206,19 @@ exports.arrayTemplater = function(container, name) { function defaultItems() { var out = []; for(var i = 0; i < templateItems.length; i++) { - if(!usedIndices[i]) { - var templateItem = templateItems[i]; - var outi = {_template: templateItem, name: templateItem.name}; + var templateItem = templateItems[i]; + var name = templateItem.name; + // only allow named items to be added as defaults, + // and only allow each name once + if(validItemName(name) && !usedNames[name]) { + var outi = { + _template: templateItem, + name: name, + _input: {_templateitemname: name} + }; outi[TEMPLATEITEMNAME] = templateItem[TEMPLATEITEMNAME]; out.push(outi); + usedNames[name] = 1; } } return out; @@ -222,6 +230,10 @@ exports.arrayTemplater = function(container, name) { }; }; +function validItemName(name) { + return name && typeof name === 'string'; +} + function arrayDefaultKey(name) { var lastChar = name.length - 1; if(name.charAt(lastChar) !== 's') { @@ -230,3 +242,82 @@ function arrayDefaultKey(name) { return name.substr(0, name.length - 1) + 'defaults'; } exports.arrayDefaultKey = arrayDefaultKey; + +/** + * arrayEditor: helper for editing array items that may have come from + * template defaults (in which case they will not exist in the input yet) + * + * @param {object} parentIn: the input container (eg gd.layout) + * @param {string} containerStr: the attribute string for the container inside + * `parentIn`. + * @param {object} itemOut: the _full* item (eg gd._fullLayout.annotations[0]) + * that we'll be editing. Assumed to have been created by `arrayTemplater`. + * + * @returns {object}: {modifyBase, modifyItem, getUpdateObj, applyUpdate}, all functions: + * modifyBase(attr, value): Add an update that's *not* related to the item. + * `attr` is the full attribute string. + * modifyItem(attr, value): Add an update to the item. `attr` is just the + * portion of the attribute string inside the item. + * getUpdateObj(): Get the final constructed update object, to use in + * `restyle` or `relayout`. Also resets the update object in case this + * update was canceled. + * applyUpdate(attr, value): optionally add an update `attr: value`, + * then apply it to `parent` which should be the parent of `containerIn`, + * ie the object to which `containerStr` is the attribute string. + */ +exports.arrayEditor = function(parentIn, containerStr, itemOut) { + var lengthIn = (Lib.nestedProperty(parentIn, containerStr).get() || []).length; + var index = itemOut._index; + // Check that we are indeed off the end of this container. + // Otherwise a devious user could put a key `_templateitemname` in their + // own input and break lots of things. + var templateItemName = (index >= lengthIn) && (itemOut._input || {})._templateitemname; + if(templateItemName) index = lengthIn; + var itemStr = containerStr + '[' + index + ']'; + + var update; + function resetUpdate() { + update = {}; + if(templateItemName) { + update[itemStr] = {}; + update[itemStr][TEMPLATEITEMNAME] = templateItemName; + } + } + resetUpdate(); + + function modifyBase(attr, value) { + update[attr] = value; + } + + function modifyItem(attr, value) { + if(templateItemName) { + // we're making a new object: edit that object + Lib.nestedProperty(update[itemStr], attr).set(value); + } + else { + // we're editing an existing object: include *just* the edit + update[itemStr + '.' + attr] = value; + } + } + + function getUpdateObj() { + var updateOut = update; + resetUpdate(); + return updateOut; + } + + function applyUpdate(attr, value) { + if(attr) modifyItem(attr, value); + var updateToApply = getUpdateObj(); + for(var key in updateToApply) { + Lib.nestedProperty(parentIn, key).set(updateToApply[key]); + } + } + + return { + modifyBase: modifyBase, + modifyItem: modifyItem, + getUpdateObj: getUpdateObj, + applyUpdate: applyUpdate + }; +}; diff --git a/src/plots/array_container_defaults.js b/src/plots/array_container_defaults.js index 65549012831..c02216e0920 100644 --- a/src/plots/array_container_defaults.js +++ b/src/plots/array_container_defaults.js @@ -61,25 +61,22 @@ module.exports = function handleArrayContainerDefaults(parentObjIn, parentObjOut } else { itemOut = templater.newItem(itemIn); - - if(itemOut.visible !== false) { - opts.handleItemDefaults(itemIn, itemOut, parentObjOut, opts, itemOpts); - } } - itemOut._input = itemIn; itemOut._index = i; + if(itemOut.visible !== false) { + opts.handleItemDefaults(itemIn, itemOut, parentObjOut, opts, itemOpts); + } + contOut.push(itemOut); } var defaultItems = templater.defaultItems(); for(i = 0; i < defaultItems.length; i++) { itemOut = defaultItems[i]; - opts.handleItemDefaults({}, itemOut, parentObjOut, opts, {}); - // TODO: we don't have an _input here - need special handling for edits, - // is that all _input is used for? itemOut._index = contOut.length; + opts.handleItemDefaults({}, itemOut, parentObjOut, opts, {}); contOut.push(itemOut); } From 43466110e63ee2a3127492a90c07a4d25be0bd6c Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 27 Jun 2018 17:08:33 -0400 Subject: [PATCH 12/24] template mock --- test/image/baselines/template.png | Bin 0 -> 56024 bytes test/image/mocks/template.json | 76 ++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 test/image/baselines/template.png create mode 100644 test/image/mocks/template.json diff --git a/test/image/baselines/template.png b/test/image/baselines/template.png new file mode 100644 index 0000000000000000000000000000000000000000..baee368fbbf4c1108e49b049f6528f9cb1797c07 GIT binary patch literal 56024 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%0Ar!G;F#$f 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 literal 0 HcmV?d00001 diff --git a/test/image/mocks/template.json b/test/image/mocks/template.json new file mode 100644 index 00000000000..e0be1630ba3 --- /dev/null +++ b/test/image/mocks/template.json @@ -0,0 +1,76 @@ +{ + "config": {"editable": true}, + "data": [ + {"y": [1, 2, 3]}, + {"y": [2, 4, 3]}, + {"y": [4, 3, 2], "xaxis": "x2", "yaxis": "y2"}, + {"type": "bar", "y": [0.5, 1, 1.5], "text": [0.5, 1, 1.5]}, + {"y": [3, 5, 4], "xaxis": "x2", "yaxis": "y2"} + ], + "layout": { + "width": 600, + "height": 500, + "yaxis2": {"anchor": "x2", "title": "y2"}, + "annotations": [ + {"text": "Hi!", "x": 0, "y": 3.5}, + {"templateitemname": "watermark", "font": {"size": 120}}, + {"templateitemname": "watermark", "font": {"size": 110}}, + {"templateitemname": "watermark", "font": {"size": 100}}, + {"templateitemname": "nope", "text": "Buh-bye"} + ], + "shapes": [ + { + "type": "line", "xref": "paper", "yref": "paper", + "x0": -0.1, "x1": 1.15, "y0": 1.05, "y1": 1.05 + } + ], + "template":{ + "data":{ + "scatter": [ + {"mode": "lines"}, + {"mode": "markers", "fill": "tonexty"} + ], + "bar": [ + {"textposition": "inside", "insidetextfont": {"color": "white"}}, + {"textposition": "outside", "outsidetextfont": {"color": "red"}} + ] + }, + "layout":{ + "colorway": ["red", "green", "blue"], + "xaxis": {"domain": [0, 0.45], "color": "#CCC", "title": "XXX"}, + "xaxis2": {"domain": [0.55, 1], "title": "XXX222"}, + "yaxis": {"color": "#88F", "title": "y"}, + "legend": {"bgcolor": "rgba(0,0,0,0)"}, + "annotationdefaults": {"arrowcolor": "#8F8", "arrowhead": 7, "ax": 0}, + "annotations": [ + { + "name": "warning", "text": "Be Cool", + "xref": "paper", "yref": "paper", "x": 1, "y": 0, + "xanchor": "left", "yanchor": "bottom", "showarrow": false + }, + { + "name": "warning2", "text": "Stay in School", + "xref": "paper", "yref": "paper", "x": 1, "y": 0, + "xanchor": "left", "yanchor": "top", "showarrow": false + }, + { + "name": "watermark", "text": "Plotly", "textangle": 25, + "xref": "paper", "yref": "paper", "x": 0.5, "y": 0.5, + "font": {"size": 40, "color": "rgba(0,0,0,0.1)"}, + "showarrow": false + } + ], + "shapedefaults": {"line": {"color": "#C60", "width": 4}}, + "shapes": [ + { + "name": "outline", "type": "rect", + "xref": "paper", "yref": "paper", + "x0": -0.15, "x1": 1.2, "y0": -0.1, "y1": 1.1, + "fillcolor": "rgba(160,160,0,0.1)", + "line": {"width": 2, "color": "rgba(160,160,0,0.25)"} + } + ] + } + } + } +} From fb489aa0fcd2c526335f09d2068f3659683ad10d Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 27 Jun 2018 22:29:23 -0400 Subject: [PATCH 13/24] tickformatstops.visible -> enabled --- src/plot_api/make_template.js | 2 ++ src/plot_api/plot_template.js | 13 +++++---- src/plots/array_container_defaults.js | 10 +++++-- src/plots/cartesian/axes.js | 4 +-- src/plots/cartesian/layout_attributes.js | 2 +- src/plots/cartesian/tick_label_defaults.js | 5 ++-- test/jasmine/tests/axes_test.js | 32 +++++++++++----------- 7 files changed, 39 insertions(+), 29 deletions(-) diff --git a/src/plot_api/make_template.js b/src/plot_api/make_template.js index 7c4df5e1cb9..404fa0491be 100644 --- a/src/plot_api/make_template.js +++ b/src/plot_api/make_template.js @@ -130,6 +130,8 @@ function mergeTemplates(oldTemplate, newTemplate) { mergeTemplates(oldVal, newVal); } else if(Array.isArray(newVal) && Array.isArray(oldVal)) { + // Note: omitted `inclusionAttr` from arrayTemplater here, + // it's irrelevant as we only want the resulting `_template`. var templater = Template.arrayTemplater({_template: oldTemplate}, key); for(j = 0; j < newVal.length; j++) { var item = newVal[j]; diff --git a/src/plot_api/plot_template.js b/src/plot_api/plot_template.js index b05ed72f1a1..e7b45f39862 100644 --- a/src/plot_api/plot_template.js +++ b/src/plot_api/plot_template.js @@ -24,7 +24,8 @@ var templateAttrs = { 'in addition to any items the figure already has in this array.', 'You can modify these items in the output figure by making your own', 'item with `templateitemname` matching this `name`', - 'alongside your modifications (including `visible: false` to hide it).', + 'alongside your modifications (including `visible: false` or', + '`enabled: false` to hide it).', 'Has no effect outside of a template.' ].join(' ') } @@ -37,8 +38,8 @@ templateAttrs[TEMPLATEITEMNAME] = { 'Used to refer to a named item in this array in the template. Named', 'items from the template will be created even without a matching item', 'in the input figure, but you can modify one by making an item with', - '`templateitemname` matching its `name`, alongside your', - 'modifications (including `visible: false` to hide it).', + '`templateitemname` matching its `name`, alongside your modifications', + '(including `visible: false` or `enabled: false` to hide it).', 'If there is no template or no matching item, this item will be', 'hidden unless you explicitly show it with `visible: true`.' ].join(' ') @@ -148,6 +149,8 @@ exports.newContainer = function(container, name, baseName) { * @param {string} name: the name of the array to template (ie 'annotations') * will be used to find default ('annotationdefaults' object) and specific * ('annotations' array) template specs. + * @param {string} inclusionAttr: the attribute determining this item's + * inclusion in the output, usually 'visible' or 'enabled' * * @returns {object}: {newItem, defaultItems}, both functions: * newItem(itemIn): create an output item, bare except for the correct @@ -156,7 +159,7 @@ exports.newContainer = function(container, name, baseName) { * specific template items that have not already beeen included, * also as bare output items ready for supplyDefaults. */ -exports.arrayTemplater = function(container, name) { +exports.arrayTemplater = function(container, name, inclusionAttr) { var template = container._template; var defaultsTemplate = template && template[arrayDefaultKey(name)]; var templateItems = template && template[name]; @@ -199,7 +202,7 @@ exports.arrayTemplater = function(container, name) { // to only be modifications it's most likely broken. Hide it unless // it's explicitly marked visible - in which case it gets NO template, // not even the default. - out.visible = itemIn.visible || false; + out[inclusionAttr] = itemIn[inclusionAttr] || false; return out; } diff --git a/src/plots/array_container_defaults.js b/src/plots/array_container_defaults.js index c02216e0920..b2600e65fd6 100644 --- a/src/plots/array_container_defaults.js +++ b/src/plots/array_container_defaults.js @@ -25,6 +25,9 @@ var Template = require('../plot_api/plot_template'); * options object: * - name {string} * name of the key linking the container in question + * - inclusionAttr {string} + * name of the item attribute for inclusion/exclusion. Default is 'visible'. + * Since inclusion is true, use eg 'enabled' instead of 'disabled'. * - handleItemDefaults {function} * defaults method to be called on each item in the array container in question * @@ -43,12 +46,13 @@ var Template = require('../plot_api/plot_template'); */ module.exports = function handleArrayContainerDefaults(parentObjIn, parentObjOut, opts) { var name = opts.name; + var inclusionAttr = opts.inclusionAttr || 'visible'; var previousContOut = parentObjOut[name]; var contIn = Lib.isArrayOrTypedArray(parentObjIn[name]) ? parentObjIn[name] : []; var contOut = parentObjOut[name] = []; - var templater = Template.arrayTemplater(parentObjOut, name); + var templater = Template.arrayTemplater(parentObjOut, name, inclusionAttr); var i, itemOut; for(i = 0; i < contIn.length; i++) { @@ -57,7 +61,7 @@ module.exports = function handleArrayContainerDefaults(parentObjIn, parentObjOut if(!Lib.isPlainObject(itemIn)) { itemOut = templater.newItem({}); - itemOut.visible = false; + itemOut[inclusionAttr] = false; } else { itemOut = templater.newItem(itemIn); @@ -65,7 +69,7 @@ module.exports = function handleArrayContainerDefaults(parentObjIn, parentObjOut itemOut._index = i; - if(itemOut.visible !== false) { + if(itemOut[inclusionAttr] !== false) { opts.handleItemDefaults(itemIn, itemOut, parentObjOut, opts, itemOpts); } diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 8c808ffa3d1..38d56e912ca 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -1396,7 +1396,7 @@ axes.getTickFormat = function(ax) { case 'linear': { for(i = 0; i < ax.tickformatstops.length; i++) { stopi = ax.tickformatstops[i]; - if(stopi.visible && isProperStop(ax.dtick, stopi.dtickrange, convertToMs)) { + if(stopi.enabled && isProperStop(ax.dtick, stopi.dtickrange, convertToMs)) { tickstop = stopi; break; } @@ -1406,7 +1406,7 @@ axes.getTickFormat = function(ax) { case 'log': { for(i = 0; i < ax.tickformatstops.length; i++) { stopi = ax.tickformatstops[i]; - if(stopi.visible && isProperLogStop(ax.dtick, stopi.dtickrange)) { + if(stopi.enabled && isProperLogStop(ax.dtick, stopi.dtickrange)) { tickstop = stopi; break; } diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index e701740a86e..1bb5a695236 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -512,7 +512,7 @@ module.exports = { ].join(' ') }, tickformatstops: templatedArray('tickformatstop', { - visible: { + enabled: { valType: 'boolean', role: 'info', dflt: true, diff --git a/src/plots/cartesian/tick_label_defaults.js b/src/plots/cartesian/tick_label_defaults.js index 8825ba7d423..c899ff91def 100644 --- a/src/plots/cartesian/tick_label_defaults.js +++ b/src/plots/cartesian/tick_label_defaults.js @@ -42,6 +42,7 @@ module.exports = function handleTickLabelDefaults(containerIn, containerOut, coe if(Array.isArray(tickformatStops) && tickformatStops.length) { handleArrayContainerDefaults(containerIn, containerOut, { name: 'tickformatstops', + inclusionAttr: 'enabled', handleItemDefaults: tickformatstopDefaults }); } @@ -89,8 +90,8 @@ function tickformatstopDefaults(valueIn, valueOut) { return Lib.coerce(valueIn, valueOut, layoutAttributes.tickformatstops, attr, dflt); } - var visible = coerce('visible'); - if(visible) { + var enabled = coerce('enabled'); + if(enabled) { coerce('dtickrange'); coerce('value'); } diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index b03b894cb98..7b6352a605e 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -2820,17 +2820,17 @@ describe('Test Axes.getTickformat', function() { it('get proper tickformatstop for linear axis', function() { var lineartickformatstops = [ { - visible: true, + enabled: true, dtickrange: [null, 1], value: '.f2', }, { - visible: true, + enabled: true, dtickrange: [1, 100], value: '.f1', }, { - visible: true, + enabled: true, dtickrange: [100, null], value: 'g', } @@ -2859,7 +2859,7 @@ describe('Test Axes.getTickformat', function() { })).toEqual(lineartickformatstops[2].value); // a stop is ignored if it's set invisible, but the others are used - lineartickformatstops[1].visible = false; + lineartickformatstops[1].enabled = false; expect(Axes.getTickFormat({ type: 'linear', tickformatstops: lineartickformatstops, @@ -2883,42 +2883,42 @@ describe('Test Axes.getTickformat', function() { var YEAR = 'M12'; // or 365.25 * DAY; var datetickformatstops = [ { - visible: true, + enabled: true, dtickrange: [null, SECOND], value: '%H:%M:%S.%L ms' // millisecond }, { - visible: true, + enabled: true, dtickrange: [SECOND, MINUTE], value: '%H:%M:%S s' // second }, { - visible: true, + enabled: true, dtickrange: [MINUTE, HOUR], value: '%H:%M m' // minute }, { - visible: true, + enabled: true, dtickrange: [HOUR, DAY], value: '%H:%M h' // hour }, { - visible: true, + enabled: true, dtickrange: [DAY, WEEK], value: '%e. %b d' // day }, { - visible: true, + enabled: true, dtickrange: [WEEK, MONTH], value: '%e. %b w' // week }, { - visible: true, + enabled: true, dtickrange: [MONTH, YEAR], value: '%b \'%y M' // month }, { - visible: true, + enabled: true, dtickrange: [YEAR, null], value: '%Y Y' // year } @@ -2969,22 +2969,22 @@ describe('Test Axes.getTickformat', function() { it('get proper tickformatstop for log axis', function() { var logtickformatstops = [ { - visible: true, + enabled: true, dtickrange: [null, 'L0.01'], value: '.f3', }, { - visible: true, + enabled: true, dtickrange: ['L0.01', 'L1'], value: '.f2', }, { - visible: true, + enabled: true, dtickrange: ['D1', 'D2'], value: '.f1', }, { - visible: true, + enabled: true, dtickrange: [1, null], value: 'g' } From bc21cc8f65b442fe440f81cd829cd23ae96dc074 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 27 Jun 2018 23:15:17 -0400 Subject: [PATCH 14/24] :hocho: done TODO --- src/plot_api/make_template.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plot_api/make_template.js b/src/plot_api/make_template.js index 404fa0491be..669ce77e84a 100644 --- a/src/plot_api/make_template.js +++ b/src/plot_api/make_template.js @@ -159,7 +159,6 @@ function walkStyleKeys(parent, templateOut, getAttributeInfo, path) { continue; } - // TODO: special array handling wrt. defaults, named items if(!attr.valType && isPlainObject(child)) { walkStyleKeys(child, templateOut, getAttributeInfo, nextPath); } From 8490804273bdef0123c5165cad67e1ff5d9882e9 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 27 Jun 2018 23:15:48 -0400 Subject: [PATCH 15/24] fix test failures - and simplify handleArrayContainerDefaults even more :tada: --- src/components/shapes/draw.js | 2 +- src/components/sliders/defaults.js | 4 ++-- src/plots/array_container_defaults.js | 5 +---- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index e52ee675513..793536597a1 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -66,7 +66,7 @@ function drawOne(gd, index) { .selectAll('.shapelayer [data-index="' + index + '"]') .remove(); - var options = gd._fullLayout.shapes[index]; + var options = gd._fullLayout.shapes[index] || {}; // this shape is gone - quit now after deleting it // TODO: use d3 idioms instead of deleting and redrawing every time diff --git a/src/components/sliders/defaults.js b/src/components/sliders/defaults.js index b2112385375..fab096c6e18 100644 --- a/src/components/sliders/defaults.js +++ b/src/components/sliders/defaults.js @@ -94,7 +94,7 @@ function sliderDefaults(sliderIn, sliderOut, layoutOut) { coerce('minorticklen'); } -function stepDefaults(valueIn, valueOut, sliderOut, opts, itemOpts) { +function stepDefaults(valueIn, valueOut) { function coerce(attr, dflt) { return Lib.coerce(valueIn, valueOut, stepAttrs, attr, dflt); } @@ -108,7 +108,7 @@ function stepDefaults(valueIn, valueOut, sliderOut, opts, itemOpts) { if(visible) { coerce('method'); coerce('args'); - var label = coerce('label', 'step-' + itemOpts.index); + var label = coerce('label', 'step-' + valueOut._index); coerce('value', label); coerce('execute'); } diff --git a/src/plots/array_container_defaults.js b/src/plots/array_container_defaults.js index b2600e65fd6..47b17e48e09 100644 --- a/src/plots/array_container_defaults.js +++ b/src/plots/array_container_defaults.js @@ -36,8 +36,6 @@ var Template = require('../plot_api/plot_template'); * - itemOut {object} item in full layout * - parentObj {object} (as in closure) * - opts {object} (as in closure) - * - itemOpts {object} - * - index {integer} * N.B. * * - opts is passed to handleItemDefaults so it can also store @@ -57,7 +55,6 @@ module.exports = function handleArrayContainerDefaults(parentObjIn, parentObjOut for(i = 0; i < contIn.length; i++) { var itemIn = contIn[i]; - var itemOpts = {}; if(!Lib.isPlainObject(itemIn)) { itemOut = templater.newItem({}); @@ -70,7 +67,7 @@ module.exports = function handleArrayContainerDefaults(parentObjIn, parentObjOut itemOut._index = i; if(itemOut[inclusionAttr] !== false) { - opts.handleItemDefaults(itemIn, itemOut, parentObjOut, opts, itemOpts); + opts.handleItemDefaults(itemIn, itemOut, parentObjOut, opts); } contOut.push(itemOut); From c2bcfe3e73d1b4f5be28259a2afc1674f0e47dcf Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sun, 1 Jul 2018 09:42:01 +0100 Subject: [PATCH 16/24] fix makeTemplate and test it & template interactions --- src/plot_api/make_template.js | 116 ++++++++--- src/plots/attributes.js | 3 +- src/plots/cartesian/layout_attributes.js | 5 + test/image/mocks/template.json | 2 +- test/jasmine/tests/plotschema_test.js | 6 +- test/jasmine/tests/template_test.js | 234 +++++++++++++++++++++-- 6 files changed, 316 insertions(+), 50 deletions(-) diff --git a/src/plot_api/make_template.js b/src/plot_api/make_template.js index 669ce77e84a..f1dd3134b2e 100644 --- a/src/plot_api/make_template.js +++ b/src/plot_api/make_template.js @@ -12,8 +12,10 @@ var Lib = require('../lib'); var isPlainObject = Lib.isPlainObject; var PlotSchema = require('./plot_schema'); +var Plots = require('../plots/plots'); var plotAttributes = require('../plots/attributes'); var Template = require('./plot_template'); +var dfltConfig = require('./plot_config'); /** * Plotly.makeTemplate: create a template off an existing figure to reuse @@ -29,8 +31,13 @@ var Template = require('./plot_template'); * `layout.template` in another figure. */ module.exports = function makeTemplate(figure) { + figure = Lib.extendDeep({_context: dfltConfig}, figure); + Plots.supplyDefaults(figure); var data = figure.data || []; var layout = figure.layout || {}; + // copy over a few items to help follow the schema + layout._basePlotModules = figure._fullLayout._basePlotModules; + layout._modules = figure._fullLayout._modules; var template = { data: {}, @@ -75,6 +82,7 @@ module.exports = function makeTemplate(figure) { * since valid options can be context-dependent. It could be solved with * a *list* of values, but that would be huge complexity for little gain. */ + delete template.layout.template; var oldTemplate = layout.template; if(isPlainObject(oldTemplate)) { var oldLayoutTemplate = oldTemplate.layout; @@ -93,9 +101,9 @@ module.exports = function makeTemplate(figure) { typeLen = typeTemplates.length; oldTypeLen = oldTypeTemplates.length; for(i = 0; i < typeLen; i++) { - mergeTemplates(oldTypeTemplates[i % typeLen], typeTemplates[i]); + mergeTemplates(oldTypeTemplates[i % oldTypeLen], typeTemplates[i]); } - for(; i < oldTypeLen; i++) { + for(i = typeLen; i < oldTypeLen; i++) { typeTemplates.push(Lib.extendDeep({}, oldTypeTemplates[i])); } } @@ -121,38 +129,72 @@ function mergeTemplates(oldTemplate, newTemplate) { var oldKeys = Object.keys(oldTemplate).sort(); var i, j; + function mergeOne(oldVal, newVal, key) { + if(isPlainObject(newVal) && isPlainObject(oldVal)) { + mergeTemplates(oldVal, newVal); + } + else if(Array.isArray(newVal) && Array.isArray(oldVal)) { + // Note: omitted `inclusionAttr` from arrayTemplater here, + // it's irrelevant as we only want the resulting `_template`. + var templater = Template.arrayTemplater({_template: oldTemplate}, key); + for(j = 0; j < newVal.length; j++) { + var item = newVal[j]; + var oldItem = templater.newItem(item)._template; + if(oldItem) mergeTemplates(oldItem, item); + } + var defaultItems = templater.defaultItems(); + for(j = 0; j < defaultItems.length; j++) newVal.push(defaultItems[j]._template); + + // templateitemname only applies to receiving plots + for(j = 0; j < newVal.length; j++) delete newVal[j].templateitemname; + } + } + for(i = 0; i < oldKeys.length; i++) { var key = oldKeys[i]; var oldVal = oldTemplate[key]; if(key in newTemplate) { - var newVal = newTemplate[key]; - if(isPlainObject(newVal) && isPlainObject(oldVal)) { - mergeTemplates(oldVal, newVal); - } - else if(Array.isArray(newVal) && Array.isArray(oldVal)) { - // Note: omitted `inclusionAttr` from arrayTemplater here, - // it's irrelevant as we only want the resulting `_template`. - var templater = Template.arrayTemplater({_template: oldTemplate}, key); - for(j = 0; j < newVal.length; j++) { - var item = newVal[j]; - var oldItem = templater.newItem(item)._template; - if(oldItem) mergeTemplates(oldItem, item); + mergeOne(oldVal, newTemplate[key], key); + } + else newTemplate[key] = oldVal; + + // if this is a base key from the old template (eg xaxis), look for + // extended keys (eg xaxis2) in the new template to merge into + if(getBaseKey(key) === key) { + for(var key2 in newTemplate) { + var baseKey2 = getBaseKey(key2); + if(key2 !== baseKey2 && baseKey2 === key && !(key2 in oldTemplate)) { + mergeOne(oldVal, newTemplate[key2], key); } - var defaultItems = templater.defaultItems(); - for(j = 0; j < defaultItems.length; j++) newVal.push(defaultItems[j]); } } - else newTemplate[key] = oldVal; } } -function walkStyleKeys(parent, templateOut, getAttributeInfo, path) { +function getBaseKey(key) { + return key.replace(/[0-9]+$/, ''); +} + +function walkStyleKeys(parent, templateOut, getAttributeInfo, path, basePath) { + var pathAttr = basePath && getAttributeInfo(basePath); for(var key in parent) { var child = parent[key]; var nextPath = getNextPath(parent, key, path); - var attr = getAttributeInfo(nextPath); + var nextBasePath = getNextPath(parent, key, basePath); + var attr = getAttributeInfo(nextBasePath); + if(!attr) { + var baseKey = getBaseKey(key); + if(baseKey !== key) { + nextBasePath = getNextPath(parent, baseKey, basePath); + attr = getAttributeInfo(nextBasePath); + } + } + + // we'll get an attr if path starts with a valid part, then has an + // invalid ending. Make sure we got all the way to the end. + if(pathAttr && (pathAttr === attr)) continue; - if(!attr || + if(!attr || attr._noTemplating || attr.valType === 'data_array' || (attr.arrayOk && Array.isArray(child)) ) { @@ -160,29 +202,47 @@ function walkStyleKeys(parent, templateOut, getAttributeInfo, path) { } if(!attr.valType && isPlainObject(child)) { - walkStyleKeys(child, templateOut, getAttributeInfo, nextPath); + walkStyleKeys(child, templateOut, getAttributeInfo, nextPath, nextBasePath); } else if(attr._isLinkedToArray && Array.isArray(child)) { var dfltDone = false; var namedIndex = 0; + var usedNames = {}; for(var i = 0; i < child.length; i++) { var item = child[i]; if(isPlainObject(item)) { - if(item.name) { - walkStyleKeys(item, templateOut, getAttributeInfo, - getNextPath(child, namedIndex, nextPath)); - namedIndex++; + var name = item.name; + if(name) { + if(!usedNames[name]) { + // named array items: allow all attributes except data arrays + walkStyleKeys(item, templateOut, getAttributeInfo, + getNextPath(child, namedIndex, nextPath), + getNextPath(child, namedIndex, nextBasePath)); + namedIndex++; + usedNames[name] = 1; + } } else if(!dfltDone) { var dfltKey = Template.arrayDefaultKey(key); - walkStyleKeys(item, templateOut, getAttributeInfo, - getNextPath(parent, dfltKey, path)); + var dfltPath = getNextPath(parent, dfltKey, path); + + // getAttributeInfo will fail if we try to use dfltKey directly. + // Instead put this item into the next array element, then + // pull it out and move it to dfltKey. + var pathInArray = getNextPath(child, namedIndex, nextPath); + walkStyleKeys(item, templateOut, getAttributeInfo, pathInArray, + getNextPath(child, namedIndex, nextBasePath)); + var itemPropInArray = Lib.nestedProperty(templateOut, pathInArray); + var dfltProp = Lib.nestedProperty(templateOut, dfltPath); + dfltProp.set(itemPropInArray.get()); + itemPropInArray.set(null); + dfltDone = true; } } } } - else if(attr.role === 'style') { + else { var templateProp = Lib.nestedProperty(templateOut, nextPath); templateProp.set(child); } diff --git a/src/plots/attributes.js b/src/plots/attributes.js index d00f27d2c2b..653176fd260 100644 --- a/src/plots/attributes.js +++ b/src/plots/attributes.js @@ -16,7 +16,8 @@ module.exports = { role: 'info', values: [], // listed dynamically dflt: 'scatter', - editType: 'calc+clearAxisTypes' + editType: 'calc+clearAxisTypes', + _noTemplating: true // we handle this at a higher level }, visible: { valType: 'enumerated', diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 1bb5a695236..f0a089c90ab 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -61,6 +61,11 @@ module.exports = { dflt: '-', role: 'info', editType: 'calc', + // we forget when an axis has been autotyped, just writing the auto + // value back to the input - so it doesn't make sense to template this. + // TODO: should we prohibit this in `coerce` as well, or honor it if + // someone enters it explicitly? + _noTemplating: true, description: [ 'Sets the axis type.', 'By default, plotly attempts to determined the axis type', diff --git a/test/image/mocks/template.json b/test/image/mocks/template.json index e0be1630ba3..6097406a41c 100644 --- a/test/image/mocks/template.json +++ b/test/image/mocks/template.json @@ -13,7 +13,7 @@ "yaxis2": {"anchor": "x2", "title": "y2"}, "annotations": [ {"text": "Hi!", "x": 0, "y": 3.5}, - {"templateitemname": "watermark", "font": {"size": 120}}, + {"templateitemname": "watermark", "font": {"size": 120}, "name": "new watermark"}, {"templateitemname": "watermark", "font": {"size": 110}}, {"templateitemname": "watermark", "font": {"size": 100}}, {"templateitemname": "nope", "text": "Buh-bye"} diff --git a/test/jasmine/tests/plotschema_test.js b/test/jasmine/tests/plotschema_test.js index 5e1c440e088..92646325066 100644 --- a/test/jasmine/tests/plotschema_test.js +++ b/test/jasmine/tests/plotschema_test.js @@ -115,7 +115,11 @@ describe('plot schema', function() { var valObject = valObjects[attr.valType], opts = valObject.requiredOpts .concat(valObject.otherOpts) - .concat(['valType', 'description', 'role', 'editType', 'impliedEdits', '_compareAsJSON']); + .concat([ + 'valType', 'description', 'role', + 'editType', 'impliedEdits', + '_compareAsJSON', '_noTemplating' + ]); Object.keys(attr).forEach(function(key) { expect(opts.indexOf(key) !== -1).toBe(true, key, attr); diff --git a/test/jasmine/tests/template_test.js b/test/jasmine/tests/template_test.js index 6fa22769479..49d7232e91d 100644 --- a/test/jasmine/tests/template_test.js +++ b/test/jasmine/tests/template_test.js @@ -3,39 +3,235 @@ var Lib = require('@src/lib'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); -var fail = require('../assets/fail_test'); +var failTest = require('../assets/fail_test'); +var drag = require('../assets/drag'); -var scatterFill = require('@mocks/scatter_fill_self_next.json'); +var scatterFillMock = require('@mocks/scatter_fill_self_next.json'); +var templateMock = require('@mocks/template.json'); describe('makeTemplate', function() { - it('does not template non-style keys', function() { - var template = Plotly.makeTemplate(scatterFill); + it('does not template arrays', function() { + var template = Plotly.makeTemplate(Lib.extendDeep({}, scatterFillMock)); expect(template).toEqual({ - data: [{fill: 'tonext'}, {fill: 'tonext'}, {fill: 'toself'}] + data: {scatter: [ + {fill: 'tonext', line: {shape: 'spline'}}, + {fill: 'tonext'}, + {fill: 'toself'} + ] }, + layout: { + title: 'Fill toself and tonext', + width: 400, + height: 400 + } }); }); - it('does not template empty layout', function() { - var template = Plotly.makeTemplate(scatterFill); - expect(template.layout).toBe(undefined); + + it('does not modify the figure while extracting a template', function() { + var mock = Lib.extendDeep({}, templateMock); + Plotly.makeTemplate(mock); + expect(mock).toEqual(templateMock); }); + it('templates scalar array_ok keys but not when they are arrays', function() { - var figure = {data: [{marker: {color: 'red'}}]}; + var figure = {data: [{ + marker: {color: 'red', size: [1, 2, 3]} + }]}; var template = Plotly.makeTemplate(figure); - expect(template.data[0].marker.color).toBe('red'); + expect(template.data.scatter[0]).toEqual({ + marker: {color: 'red'} + }); + }); + it('does not template invalid keys but does template invalid values', function() { + var figure = {data: [{ + marker: {fugacity: 2, size: 'tiny'}, + smell: 'fruity' + }]}; + var template = Plotly.makeTemplate(figure); + expect(template.data.scatter[0]).toEqual({ + marker: {size: 'tiny'} + }); + }); + + it('pulls the first unnamed array item as defaults, plus one item of each distinct name', function() { + var figure = { + layout: { + annotations: [ + {name: 'abc', text: 'whee!'}, + {text: 'boo', bgcolor: 'blue'}, + {text: 'hoo', x: 1, y: 2}, + {name: 'def', text: 'yoink', x: 3, y: 4}, + {name: 'abc', x: 5, y: 6} + ] + } + }; + var template = Plotly.makeTemplate(figure); + expect(template.layout).toEqual({ + annotationdefaults: {text: 'boo', bgcolor: 'blue'}, + annotations: [ + {name: 'abc', text: 'whee!'}, + {name: 'def', text: 'yoink', x: 3, y: 4} + ] + }); }); -}); -describe('applyTemplate', function() { - it('forces default values for keys not present in template', function() { - var template = { - data: [{fill: 'tonext'}, {fill: 'tonext'}, {fill: 'toself'}] + it('merges in the template that was already in the figure', function() { + var mock = Lib.extendDeep({}, templateMock); + var template = Plotly.makeTemplate(mock); + + var expected = { + data: { + scatter: [ + {mode: 'lines'}, + {fill: 'tonexty', mode: 'markers'}, + {mode: 'lines', xaxis: 'x2', yaxis: 'y2'}, + {fill: 'tonexty', mode: 'markers', xaxis: 'x2', yaxis: 'y2'} + ], + bar: [ + {insidetextfont: {color: 'white'}, textposition: 'inside'}, + {textposition: 'outside', outsidetextfont: {color: 'red'}} + ] + }, + layout: { + annotationdefaults: { + arrowcolor: '#8F8', arrowhead: 7, ax: 0, + text: 'Hi!', x: 0, y: 3.5 + }, + annotations: [{ + // new name & font size vs the original template + name: 'new watermark', text: 'Plotly', textangle: 25, + xref: 'paper', yref: 'paper', x: 0.5, y: 0.5, + font: {size: 120, color: 'rgba(0,0,0,0.1)'}, + showarrow: false + }, { + name: 'warning', text: 'Be Cool', + xref: 'paper', yref: 'paper', x: 1, y: 0, + xanchor: 'left', yanchor: 'bottom', showarrow: false + }, { + name: 'warning2', text: 'Stay in School', + xref: 'paper', yref: 'paper', x: 1, y: 0, + xanchor: 'left', yanchor: 'top', showarrow: false + }], + colorway: ['red', 'green', 'blue'], + height: 500, + legend: {bgcolor: 'rgba(0,0,0,0)'}, + // inherits from shapes[0] and template.shapedefaults + shapedefaults: { + type: 'line', + x0: -0.1, x1: 1.15, y0: 1.05, y1: 1.05, + xref: 'paper', yref: 'paper', + line: {color: '#C60', width: 4} + }, + shapes: [{ + name: 'outline', type: 'rect', + xref: 'paper', yref: 'paper', + x0: -0.15, x1: 1.2, y0: -0.1, y1: 1.1, + fillcolor: 'rgba(160,160,0,0.1)', + line: {width: 2, color: 'rgba(160,160,0,0.25)'} + }], + width: 600, + xaxis: {domain: [0, 0.45], color: '#CCC', title: 'XXX'}, + xaxis2: {domain: [0.55, 1], title: 'XXX222'}, + yaxis: {color: '#88F', title: 'y'}, + // inherits from both yaxis2 and template.yaxis + yaxis2: {color: '#88F', title: 'y2', anchor: 'x2'} + } }; - var figure = Lib.extendDeepAll({}, scatterFill); - var results = Plotly.applyTemplate(figure, template); - var templatedFigure = results.templatedFigure; - expect(templatedFigure).toEqual(figure); + expect(template).toEqual(expected); + }); +}); + +// statics of template application are all covered by the template mock +// but we still need to manage the interactions +describe('template interactions', function() { + var gd; + + beforeEach(function(done) { + var mock = Lib.extendDeep({}, templateMock); + gd = createGraphDiv(); + Plotly.newPlot(gd, mock) + .catch(failTest) + .then(done); + }); + afterEach(destroyGraphDiv); + + it('makes a new annotation or edits the existing one as necessary', function(done) { + function checkAnnotations(layoutCount, coolIndex, schoolIndex, schooly) { + expect(gd.layout.annotations.length).toBe(layoutCount); + expect(gd._fullLayout.annotations.length).toBe(7); + var annotationElements = document.querySelectorAll('.annotation'); + var coolElement = annotationElements[coolIndex]; + var schoolElement = annotationElements[schoolIndex]; + expect(annotationElements.length).toBe(6); // one hidden + expect(coolElement.textContent).toBe('Be Cool'); + expect(schoolElement.textContent).toBe('Stay in School'); + + if(schooly) { + var schoolItem = gd.layout.annotations[layoutCount - 1]; + expect(schoolItem).toEqual(jasmine.objectContaining({ + templateitemname: 'warning2', + x: 1 + })); + expect(schoolItem.y).toBeWithin(schooly, 0.001); + } + + return schoolElement.querySelector('.cursor-pointer'); + } + + var schoolDragger = checkAnnotations(5, 4, 5); + + drag(schoolDragger, 0, -80) + .then(function() { + // added an item to layout.annotations and put that before the + // remaining default item in the DOM + schoolDragger = checkAnnotations(6, 5, 4, 0.25); + + return drag(schoolDragger, 0, -80); + }) + .then(function() { + // item count and order are unchanged now, item just moves. + schoolDragger = checkAnnotations(6, 5, 4, 0.5); + }) + .catch(failTest) + .then(done); + }); + + it('makes a new shape or edits the existing one as necessary', function(done) { + function checkShapes(layoutCount, recty0) { + expect(gd.layout.shapes.length).toBe(layoutCount); + expect(gd._fullLayout.shapes.length).toBe(2); + var shapeElements = document.querySelectorAll('.shapelayer path[fill-rule=\'evenodd\']'); + var rectElement = shapeElements[1]; + expect(shapeElements.length).toBe(2); + + if(recty0) { + var rectItem = gd.layout.shapes[layoutCount - 1]; + expect(rectItem).toEqual(jasmine.objectContaining({ + templateitemname: 'outline', + x0: -0.15, x1: 1.2, y1: 1.1 + })); + expect(rectItem.y0).toBeWithin(recty0, 0.001); + } + + return rectElement; + } + + var rectDragger = checkShapes(1); + + drag(rectDragger, 0, -80, 's') + .then(function() { + // added an item to layout.shapes + rectDragger = checkShapes(2, 0.15); + + return drag(rectDragger, 0, -80, 's'); + }) + .then(function() { + // item count and order are unchanged now, item just resizes. + rectDragger = checkShapes(2, 0.4); + }) + .catch(failTest) + .then(done); }); }); From 890a324b330eb4fa5e2c3bcb999ac44087a6c54a Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 2 Jul 2018 16:05:30 +0200 Subject: [PATCH 17/24] fix Plotly.validate with attributes that end in numbers --- src/plot_api/validate.js | 4 +++- test/image/mocks/shapes.json | 2 +- test/jasmine/tests/validate_test.js | 8 ++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/plot_api/validate.js b/src/plot_api/validate.js index 0baf1649182..dc77673cc74 100644 --- a/src/plot_api/validate.js +++ b/src/plot_api/validate.js @@ -38,7 +38,7 @@ var isArrayOrTypedArray = Lib.isArrayOrTypedArray; * - {string} msg * error message (shown in console in logger config argument is enable) */ -module.exports = function valiate(data, layout) { +module.exports = function validate(data, layout) { var schema = PlotSchema.get(); var errorList = []; var gd = {_context: Lib.extendFlat({}, dfltConfig)}; @@ -397,6 +397,8 @@ function isInSchema(schema, key) { } function getNestedSchema(schema, key) { + if(key in schema) return schema[key]; + var parts = splitKey(key); return schema[parts.keyMinusId]; diff --git a/test/image/mocks/shapes.json b/test/image/mocks/shapes.json index e5ba275b48d..2aadf3f0020 100644 --- a/test/image/mocks/shapes.json +++ b/test/image/mocks/shapes.json @@ -18,7 +18,7 @@ "yaxis2":{"title":"category","range":[0,1],"domain":[0.6,1],"anchor":"x2","type":"category","showgrid":false,"zeroline":false,"showticklabels":false}, "height":400, "width":800, - "margin": {"l":20,"r":20,"top":10,"bottom":10,"pad":0}, + "margin": {"l":20,"r":20,"pad":0}, "showlegend":false, "shapes":[ {"layer":"below","xref":"paper","yref":"paper","x0":0,"x1":0.1,"y0":0,"y1":0.1}, diff --git a/test/jasmine/tests/validate_test.js b/test/jasmine/tests/validate_test.js index 4d1d246ddb0..0c1074fd250 100644 --- a/test/jasmine/tests/validate_test.js +++ b/test/jasmine/tests/validate_test.js @@ -531,4 +531,12 @@ describe('Plotly.validate', function() { expect(out).toBeUndefined(); }); + + it('should accept attributes that really end in a number', function() { + // and not try to strip that number off! + // eg x0, x1 in shapes + var shapeMock = require('@mocks/shapes.json'); + var out = Plotly.validate(shapeMock.data, shapeMock.layout); + expect(out).toBeUndefined(); + }); }); From aed44dc8b41f9312ab949540b86a214a5123ee80 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 3 Jul 2018 15:02:25 +0200 Subject: [PATCH 18/24] Plotly.validateTemplate --- src/plot_api/index.js | 5 +- src/plot_api/plot_template.js | 2 + .../{make_template.js => template_api.js} | 186 +++++++++++++++++- test/jasmine/tests/template_test.js | 103 ++++++++++ 4 files changed, 294 insertions(+), 2 deletions(-) rename src/plot_api/{make_template.js => template_api.js} (62%) diff --git a/src/plot_api/index.js b/src/plot_api/index.js index 1d5f4d633ae..ac81c327b05 100644 --- a/src/plot_api/index.js +++ b/src/plot_api/index.js @@ -31,4 +31,7 @@ exports.setPlotConfig = main.setPlotConfig; exports.toImage = require('./to_image'); exports.validate = require('./validate'); exports.downloadImage = require('../snapshot/download'); -exports.makeTemplate = require('./make_template'); + +var templateApi = require('./template_api'); +exports.makeTemplate = templateApi.makeTemplate; +exports.validateTemplate = templateApi.validateTemplate; diff --git a/src/plot_api/plot_template.js b/src/plot_api/plot_template.js index e7b45f39862..fe6b5d4aad1 100644 --- a/src/plot_api/plot_template.js +++ b/src/plot_api/plot_template.js @@ -203,6 +203,8 @@ exports.arrayTemplater = function(container, name, inclusionAttr) { // it's explicitly marked visible - in which case it gets NO template, // not even the default. out[inclusionAttr] = itemIn[inclusionAttr] || false; + // special falsy value we can look for in validateTemplate + out._template = false; return out; } diff --git a/src/plot_api/make_template.js b/src/plot_api/template_api.js similarity index 62% rename from src/plot_api/make_template.js rename to src/plot_api/template_api.js index f1dd3134b2e..084dc7238bd 100644 --- a/src/plot_api/make_template.js +++ b/src/plot_api/template_api.js @@ -30,7 +30,7 @@ var dfltConfig = require('./plot_config'); * @returns {object} template: the extracted template - can then be used as * `layout.template` in another figure. */ -module.exports = function makeTemplate(figure) { +exports.makeTemplate = function(figure) { figure = Lib.extendDeep({_context: dfltConfig}, figure); Plots.supplyDefaults(figure); var data = figure.data || []; @@ -269,3 +269,187 @@ function getNextPath(parent, key, path) { return nextPath; } + +/** + * validateTemplate: Test for consistency between the given figure and + * a template, either already included in the figure or given separately. + * Note that not every issue we identify here is necessarily a problem, + * it depends on what you're using the template for. + * + * @param {object|DOM element} figure: the plot, with {data, layout} members, + * to test the template against + * @param {Optional(object)} template: the template, with its own {data, layout}, + * to test. If omitted, we will look for a template already attached as the + * plot's `layout.template` attribute. + * + * @returns {array} array of error objects each containing: + * - {string} code + * error code ('missing', 'unused', 'reused', 'noLayout', 'noData') + * - {string} msg + * a full readable description of the issue. + */ +exports.validateTemplate = function(figureIn, template) { + var figure = Lib.extendDeep({}, { + _context: dfltConfig, + data: figureIn.data, + layout: figureIn.layout + }); + var layout = figure.layout || {}; + if(!isPlainObject(template)) template = layout.template || {}; + var layoutTemplate = template.layout; + var dataTemplate = template.data; + var errorList = []; + + figure.layout = layout; + figure.layout.template = template; + Plots.supplyDefaults(figure); + + var fullLayout = figure._fullLayout; + var fullData = figure._fullData; + + if(!isPlainObject(layoutTemplate)) { + errorList.push({code: 'layout'}); + } + else { + // TODO: any need to look deeper than the first level of layout? + // I don't think so, that gets all the subplot types which should be + // sufficient. + for(var key in layoutTemplate) { + if(key.indexOf('defaults') === -1 && isPlainObject(layoutTemplate[key]) && + !hasMatchingKey(fullLayout, key) + ) { + errorList.push({code: 'unused', path: 'layout.' + key}); + } + } + } + + if(!isPlainObject(dataTemplate)) { + errorList.push({code: 'data'}); + } + else { + var typeCount = {}; + var traceType; + for(var i = 0; i < fullData.length; i++) { + var fullTrace = fullData[i]; + traceType = fullTrace.type; + typeCount[traceType] = (typeCount[traceType] || 0) + 1; + if(!fullTrace._fullInput._template) { + // this takes care of the case of traceType in the data but not + // the template + errorList.push({ + code: 'missing', + index: fullTrace._fullInput.index, + traceType: traceType + }); + } + } + for(traceType in dataTemplate) { + var templateCount = dataTemplate[traceType].length; + var dataCount = typeCount[traceType] || 0; + if(templateCount > dataCount) { + errorList.push({ + code: 'unused', + traceType: traceType, + templateCount: templateCount, + dataCount: dataCount + }); + } + else if(dataCount > templateCount) { + errorList.push({ + code: 'reused', + traceType: traceType, + templateCount: templateCount, + dataCount: dataCount + }); + } + } + } + + // _template: false is when someone tried to modify an array item + // but there was no template with matching name + function crawlForMissingTemplates(obj, path) { + for(var key in obj) { + if(key.charAt(0) === '_') continue; + var val = obj[key]; + var nextPath = getNextPath(obj, key, path); + if(isPlainObject(val)) { + if(Array.isArray(obj) && val._template === false && val.templateitemname) { + errorList.push({ + code: 'missing', + path: nextPath, + templateitemname: val.templateitemname + }); + } + crawlForMissingTemplates(val, nextPath); + } + else if(Array.isArray(val) && hasPlainObject(val)) { + crawlForMissingTemplates(val, nextPath); + } + } + } + crawlForMissingTemplates({data: fullData, layout: fullLayout}, ''); + + if(errorList.length) return errorList.map(format); +}; + +function hasPlainObject(arr) { + for(var i = 0; i < arr.length; i++) { + if(isPlainObject(arr[i])) return true; + } +} + +function hasMatchingKey(obj, key) { + if(key in obj) return true; + if(getBaseKey(key) !== key) return false; + for(var key2 in obj) { + if(getBaseKey(key2) === key) return true; + } +} + +function format(opts) { + var msg; + switch(opts.code) { + case 'data': + msg = 'The template has no key data.'; + break; + case 'layout': + msg = 'The template has no key layout.'; + break; + case 'missing': + if(opts.path) { + msg = 'There are no templates for item ' + opts.path + + ' with name ' + opts.templateitemname; + } + else { + msg = 'There are no templates for trace ' + opts.index + + ', of type ' + opts.traceType + '.'; + } + break; + case 'unused': + if(opts.path) { + msg = 'The template item at ' + opts.path + + ' was not used in constructing the plot.'; + } + else if(opts.dataCount) { + msg = 'Some of the templates of type ' + opts.traceType + + ' were not used. The template has ' + opts.templateCount + + ' traces, the data only has ' + opts.dataCount + + ' of this type.'; + } + else { + msg = 'The template has ' + opts.templateCount + + ' traces of type ' + opts.traceType + + ' but there are none in the data.'; + } + break; + case 'reused': + msg = 'Some of the templates of type ' + opts.traceType + + ' were used more than once. The template has ' + + opts.templateCount + ' traces, the data has ' + + opts.dataCount + ' of this type.'; + break; + } + opts.msg = msg; + + return opts; +} diff --git a/test/jasmine/tests/template_test.js b/test/jasmine/tests/template_test.js index 49d7232e91d..66dc604b1dd 100644 --- a/test/jasmine/tests/template_test.js +++ b/test/jasmine/tests/template_test.js @@ -235,3 +235,106 @@ describe('template interactions', function() { .then(done); }); }); + +describe('validateTemplate', function() { + + function checkValidate(mock, expected, countToCheck) { + var template = mock.layout.template; + var mockNoTemplate = Lib.extendDeep({}, mock); + delete mockNoTemplate.layout.template; + + var out1 = Plotly.validateTemplate(mock); + var out2 = Plotly.validateTemplate(mockNoTemplate, template); + expect(out2).toEqual(out1); + if(expected) { + expect(countToCheck ? out1.slice(0, countToCheck) : out1) + .toEqual(expected); + } + else { + expect(out1).toBeUndefined(); + } + } + + var cleanMock = Lib.extendDeep({}, templateMock); + cleanMock.layout.annotations.pop(); + cleanMock.data.pop(); + cleanMock.data.splice(1, 1); + cleanMock.layout.template.data.bar.pop(); + + it('returns undefined when the template matches precisely', function() { + checkValidate(cleanMock); + }); + + it('catches all classes of regular issue', function() { + var messyMock = Lib.extendDeep({}, templateMock); + messyMock.data.push({type: 'box', x0: 1, y: [1, 2, 3]}); + messyMock.layout.template.layout.geo = {projection: {type: 'orthographic'}}; + messyMock.layout.template.layout.xaxis3 = {nticks: 50}; + messyMock.layout.template.data.violin = [{fillcolor: '#000'}]; + + checkValidate(messyMock, [{ + code: 'unused', + path: 'layout.geo', + msg: 'The template item at layout.geo was not used in constructing the plot.' + }, { + code: 'unused', + path: 'layout.xaxis3', + msg: 'The template item at layout.xaxis3 was not used in constructing the plot.' + }, { + code: 'missing', + index: 5, + traceType: 'box', + msg: 'There are no templates for trace 5, of type box.' + }, { + code: 'reused', + traceType: 'scatter', + templateCount: 2, + dataCount: 4, + msg: 'Some of the templates of type scatter were used more than once.' + + ' The template has 2 traces, the data has 4 of this type.' + }, { + code: 'unused', + traceType: 'bar', + templateCount: 2, + dataCount: 1, + msg: 'Some of the templates of type bar were not used.' + + ' The template has 2 traces, the data only has 1 of this type.' + }, { + code: 'unused', + traceType: 'violin', + templateCount: 1, + dataCount: 0, + msg: 'The template has 1 traces of type violin' + + ' but there are none in the data.' + }, { + code: 'missing', + path: 'layout.annotations[4]', + templateitemname: 'nope', + msg: 'There are no templates for item layout.annotations[4] with name nope' + }]); + }); + + it('catches missing template.data', function() { + var noDataMock = Lib.extendDeep({}, cleanMock); + delete noDataMock.layout.template.data; + + checkValidate(noDataMock, [{ + code: 'data', + msg: 'The template has no key data.' + }], + // check only the first error - we don't care about the specifics + // uncovered after we already know there's no template.data + 1); + }); + + it('catches missing template.data', function() { + var noLayoutMock = Lib.extendDeep({}, cleanMock); + delete noLayoutMock.layout.template.layout; + + checkValidate(noLayoutMock, [{ + code: 'layout', + msg: 'The template has no key layout.' + }], 1); + }); + +}); From 6df61e0d31f5eb043b20ea4e945a7238c5146fa4 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 3 Jul 2018 15:31:35 +0200 Subject: [PATCH 19/24] loosen template default item interaction tests to pass on CI --- test/jasmine/tests/template_test.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/test/jasmine/tests/template_test.js b/test/jasmine/tests/template_test.js index 66dc604b1dd..b34c0f2b90a 100644 --- a/test/jasmine/tests/template_test.js +++ b/test/jasmine/tests/template_test.js @@ -170,10 +170,8 @@ describe('template interactions', function() { if(schooly) { var schoolItem = gd.layout.annotations[layoutCount - 1]; - expect(schoolItem).toEqual(jasmine.objectContaining({ - templateitemname: 'warning2', - x: 1 - })); + expect(schoolItem.templateitemname).toBe('warning2'); + expect(schoolItem.x).toBeWithin(1, 0.001); expect(schoolItem.y).toBeWithin(schooly, 0.001); } @@ -208,11 +206,11 @@ describe('template interactions', function() { if(recty0) { var rectItem = gd.layout.shapes[layoutCount - 1]; - expect(rectItem).toEqual(jasmine.objectContaining({ - templateitemname: 'outline', - x0: -0.15, x1: 1.2, y1: 1.1 - })); + expect(rectItem.templateitemname).toBe('outline'); + expect(rectItem.x0).toBeWithin(-0.15, 0.001); expect(rectItem.y0).toBeWithin(recty0, 0.001); + expect(rectItem.x1).toBeWithin(1.2, 0.001); + expect(rectItem.y1).toBeWithin(1.1, 0.001); } return rectElement; From ef4c3cc3c63ad647de39a145b59425a503e15edf Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 3 Jul 2018 16:30:16 +0200 Subject: [PATCH 20/24] TODO -> note re: axis.type _noTemplate --- src/plots/cartesian/layout_attributes.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index f0a089c90ab..33971a84d24 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -63,8 +63,8 @@ module.exports = { editType: 'calc', // we forget when an axis has been autotyped, just writing the auto // value back to the input - so it doesn't make sense to template this. - // TODO: should we prohibit this in `coerce` as well, or honor it if - // someone enters it explicitly? + // Note: we do NOT prohibit this in `coerce`, so if someone enters a + // type in the template explicitly it will be honored as the default. _noTemplating: true, description: [ 'Sets the axis type.', From b1c6f0ae0c0d47dab78df8255cd373316a3c3a97 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 3 Jul 2018 18:15:47 +0200 Subject: [PATCH 21/24] _noTemplating for angularaxis.type --- src/plots/polar/layout_attributes.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plots/polar/layout_attributes.js b/src/plots/polar/layout_attributes.js index 841f984399a..ab2c880daeb 100644 --- a/src/plots/polar/layout_attributes.js +++ b/src/plots/polar/layout_attributes.js @@ -148,6 +148,7 @@ var angularAxisAttrs = { dflt: '-', role: 'info', editType: 'calc', + _noTemplating: true, description: [ 'Sets the angular axis type.', 'If *linear*, set `thetaunit` to determine the unit in which axis value are shown.', From 818cac94c8c96dc9fe39eef143f605ea12ee5966 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 3 Jul 2018 18:16:35 +0200 Subject: [PATCH 22/24] :cow2: test name typo --- test/jasmine/tests/template_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jasmine/tests/template_test.js b/test/jasmine/tests/template_test.js index b34c0f2b90a..f826f4ae16f 100644 --- a/test/jasmine/tests/template_test.js +++ b/test/jasmine/tests/template_test.js @@ -325,7 +325,7 @@ describe('validateTemplate', function() { 1); }); - it('catches missing template.data', function() { + it('catches missing template.layout', function() { var noLayoutMock = Lib.extendDeep({}, cleanMock); delete noLayoutMock.layout.template.layout; From 15931cf17ebb77baa108b85d80237bfd40cec700 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 3 Jul 2018 18:17:20 +0200 Subject: [PATCH 23/24] recurse into (template)layout looking for unused containers --- src/plot_api/template_api.js | 45 ++++++++++++++++++++++------- test/jasmine/tests/template_test.js | 11 +++++++ 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/src/plot_api/template_api.js b/src/plot_api/template_api.js index 084dc7238bd..c41641faba0 100644 --- a/src/plot_api/template_api.js +++ b/src/plot_api/template_api.js @@ -307,20 +307,45 @@ exports.validateTemplate = function(figureIn, template) { var fullLayout = figure._fullLayout; var fullData = figure._fullData; + var layoutPaths = {}; + function crawlLayoutForContainers(obj, paths) { + for(var key in obj) { + if(key.charAt(0) !== '_' && isPlainObject(obj[key])) { + var baseKey = getBaseKey(key); + var nextPaths = []; + var i; + for(i = 0; i < paths.length; i++) { + nextPaths.push(getNextPath(obj, key, paths[i])); + if(baseKey !== key) nextPaths.push(getNextPath(obj, baseKey, paths[i])); + } + for(i = 0; i < nextPaths.length; i++) { + layoutPaths[nextPaths[i]] = 1; + } + crawlLayoutForContainers(obj[key], nextPaths); + } + } + } + + function crawlLayoutTemplateForContainers(obj, path) { + for(var key in obj) { + if(key.indexOf('defaults') === -1 && isPlainObject(obj[key])) { + var nextPath = getNextPath(obj, key, path); + if(layoutPaths[nextPath]) { + crawlLayoutTemplateForContainers(obj[key], nextPath); + } + else { + errorList.push({code: 'unused', path: nextPath}); + } + } + } + } + if(!isPlainObject(layoutTemplate)) { errorList.push({code: 'layout'}); } else { - // TODO: any need to look deeper than the first level of layout? - // I don't think so, that gets all the subplot types which should be - // sufficient. - for(var key in layoutTemplate) { - if(key.indexOf('defaults') === -1 && isPlainObject(layoutTemplate[key]) && - !hasMatchingKey(fullLayout, key) - ) { - errorList.push({code: 'unused', path: 'layout.' + key}); - } - } + crawlLayoutForContainers(fullLayout, ['layout']); + crawlLayoutTemplateForContainers(layoutTemplate, 'layout'); } if(!isPlainObject(dataTemplate)) { diff --git a/test/jasmine/tests/template_test.js b/test/jasmine/tests/template_test.js index f826f4ae16f..81ffe8f7e2f 100644 --- a/test/jasmine/tests/template_test.js +++ b/test/jasmine/tests/template_test.js @@ -268,9 +268,20 @@ describe('validateTemplate', function() { messyMock.data.push({type: 'box', x0: 1, y: [1, 2, 3]}); messyMock.layout.template.layout.geo = {projection: {type: 'orthographic'}}; messyMock.layout.template.layout.xaxis3 = {nticks: 50}; + messyMock.layout.template.layout.xaxis.rangeslider = {yaxis3: {rangemode: 'fixed'}}; + messyMock.layout.xaxis = {rangeslider: {}}; + messyMock.layout.template.layout.xaxis2.rangeslider = {bgcolor: '#CCC'}; messyMock.layout.template.data.violin = [{fillcolor: '#000'}]; checkValidate(messyMock, [{ + code: 'unused', + path: 'layout.xaxis.rangeslider.yaxis3', + msg: 'The template item at layout.xaxis.rangeslider.yaxis3 was not used in constructing the plot.' + }, { + code: 'unused', + path: 'layout.xaxis2.rangeslider', + msg: 'The template item at layout.xaxis2.rangeslider was not used in constructing the plot.' + }, { code: 'unused', path: 'layout.geo', msg: 'The template item at layout.geo was not used in constructing the plot.' From 8598bc95afa6ed941433642c1a34b416dba0f57d Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 3 Jul 2018 18:25:42 +0200 Subject: [PATCH 24/24] :hocho: obsolete code --- src/plot_api/template_api.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/plot_api/template_api.js b/src/plot_api/template_api.js index c41641faba0..a9eaa84b167 100644 --- a/src/plot_api/template_api.js +++ b/src/plot_api/template_api.js @@ -423,14 +423,6 @@ function hasPlainObject(arr) { } } -function hasMatchingKey(obj, key) { - if(key in obj) return true; - if(getBaseKey(key) !== key) return false; - for(var key2 in obj) { - if(getBaseKey(key2) === key) return true; - } -} - function format(opts) { var msg; switch(opts.code) {