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); 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/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/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/defaults.js b/src/components/annotations/defaults.js index c101813501f..2ca55c44bae 100644 --- a/src/components/annotations/defaults.js +++ b/src/components/annotations/defaults.js @@ -9,15 +9,91 @@ '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) { + function coerce(attr, dflt) { + return Lib.coerce(annIn, annOut, attributes, attr, dflt); + } + + var visible = coerce('visible'); + var clickToShow = coerce('clicktoshow'); + + if(!(visible || clickToShow)) return; + + 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); + } +} 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/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/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/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/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/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/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 bdc3d6f86d8..00000000000 --- a/src/components/rangeselector/button_attributes.js +++ /dev/null @@ -1,61 +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 = { - 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 741dfacb5d1..0d5e95f64a7 100644 --- a/src/components/rangeselector/defaults.js +++ b/src/components/rangeselector/defaults.js @@ -10,57 +10,56 @@ 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'); 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); } - 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; - - 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 buttonsDefaults(containerIn, containerOut, calendar) { - var buttonsIn = containerIn.buttons || [], - buttonsOut = containerOut.buttons = []; - - var buttonIn, buttonOut; +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); } - for(var i = 0; i < buttonsIn.length; i++) { - buttonIn = buttonsIn[i]; - buttonOut = {}; - - if(!Lib.isPlainObject(buttonIn)) continue; + var visible = coerce('visible'); + if(visible) { var step = coerce('step'); if(step !== 'all') { if(calendar && calendar !== 'gregorian' && (step === 'month' || step === 'year')) { @@ -74,12 +73,7 @@ function buttonsDefaults(containerIn, containerOut, calendar) { } coerce('label'); - - buttonOut._index = i; - buttonsOut.push(buttonOut); } - - return buttonsOut; } function getPosDflt(containerOut, layout, counterAxes) { 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/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/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/shapes/defaults.js b/src/components/shapes/defaults.js index bcb010760fd..bc2934e0fee 100644 --- a/src/components/shapes/defaults.js +++ b/src/components/shapes/defaults.js @@ -9,15 +9,114 @@ '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) { + function coerce(attr, dflt) { + return Lib.coerce(shapeIn, shapeOut, attributes, attr, dflt); + } + + var visible = coerce('visible'); + + if(!visible) return; + + 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']); + } +} diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index fef3eb31931..793536597a1 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/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; -}; diff --git a/src/components/sliders/attributes.js b/src/components/sliders/attributes.js index 3edfee97767..574dee98e9d 100644 --- a/src/components/sliders/attributes.js +++ b/src/components/sliders/attributes.js @@ -13,11 +13,18 @@ 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', + dflt: true, + description: [ + 'Determines whether or not this step is included in the slider.' + ].join(' ') + }, method: { valType: 'enumerated', values: ['restyle', 'relayout', 'animate', 'update', 'skip'], @@ -70,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', @@ -285,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/sliders/defaults.js b/src/components/sliders/defaults.js index 42fe3aa8345..fab096c6e18 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,22 @@ 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) { function coerce(attr, dflt) { return Lib.coerce(valueIn, valueOut, stepAttrs, attr, dflt); } - for(var i = 0; i < valuesIn.length; i++) { - valueIn = valuesIn[i]; - valueOut = {}; + var visible; + if(valueIn.method !== 'skip' && !Array.isArray(valueIn.args)) { + visible = valueOut.visible = false; + } + else visible = coerce('visible'); + if(visible) { coerce('method'); - - if(!Lib.isPlainObject(valueIn) || (valueOut.method !== 'skip' && !Array.isArray(valueIn.args))) { - continue; - } - coerce('args'); - coerce('label', 'step-' + i); - coerce('value', valueOut.label); + var label = coerce('label', 'step-' + valueOut._index); + coerce('value', label); coerce('execute'); - - valuesOut.push(valueOut); } - - return valuesOut; } diff --git a/src/components/sliders/draw.js b/src/components/sliders/draw.js index 512f7fef9fc..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'); @@ -74,14 +75,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 +109,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 +125,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 +174,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 +258,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 +276,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,20 +404,25 @@ 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); } } 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]; - 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 +505,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 +527,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 +537,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..6224392ed02 100644 --- a/src/components/updatemenus/attributes.js +++ b/src/components/updatemenus/attributes.js @@ -13,10 +13,14 @@ 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', + description: 'Determines whether or not this button is visible.' + }, method: { valType: 'enumerated', values: ['restyle', 'relayout', 'animate', 'update', 'skip'], @@ -62,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: { @@ -186,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/components/updatemenus/defaults.js b/src/components/updatemenus/defaults.js index a3fbb0c3260..1cce74a2c96 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,33 +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) { 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', + (buttonIn.method === 'skip' || Array.isArray(buttonIn.args))); + if(visible) { coerce('method'); - - if(!Lib.isPlainObject(buttonIn) || (buttonOut.method !== 'skip' && !Array.isArray(buttonIn.args))) { - continue; - } - coerce('args'); coerce('label'); coerce('execute'); - - buttonOut._index = i; - buttonsOut.push(buttonOut); } - - return buttonsOut; } diff --git a/src/components/updatemenus/draw.js b/src/components/updatemenus/draw.js index e72aa49231e..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; @@ -25,7 +26,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 +138,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, @@ -171,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); @@ -255,7 +244,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 +487,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/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..ac81c327b05 100644 --- a/src/plot_api/index.js +++ b/src/plot_api/index.js @@ -31,3 +31,7 @@ exports.setPlotConfig = main.setPlotConfig; exports.toImage = require('./to_image'); exports.validate = require('./validate'); exports.downloadImage = require('../snapshot/download'); + +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 new file mode 100644 index 00000000000..fe6b5d4aad1 --- /dev/null +++ b/src/plot_api/plot_template.js @@ -0,0 +1,328 @@ +/** +* 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 = '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` or', + '`enabled: 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` 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(' ') +}; + +/** + * 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 + * + * @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. + * @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 + * 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, inclusionAttr) { + var template = container._template; + var defaultsTemplate = template && template[arrayDefaultKey(name)]; + var templateItems = template && template[name]; + if(!Array.isArray(templateItems) || !templateItems.length) { + templateItems = []; + } + + var usedNames = {}; + + 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, _input: itemIn}; + var templateItemName = out[TEMPLATEITEMNAME] = itemIn[TEMPLATEITEMNAME]; + + // no itemname: use the default template + if(!validItemName(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. + usedNames[templateItemName] = 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[inclusionAttr] = itemIn[inclusionAttr] || false; + // special falsy value we can look for in validateTemplate + out._template = false; + return out; + } + + function defaultItems() { + var out = []; + for(var i = 0; i < templateItems.length; i++) { + 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; + } + + return { + newItem: newItem, + defaultItems: defaultItems + }; +}; + +function validItemName(name) { + return name && typeof name === 'string'; +} + +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; + +/** + * 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/plot_api/template_api.js b/src/plot_api/template_api.js new file mode 100644 index 00000000000..a9eaa84b167 --- /dev/null +++ b/src/plot_api/template_api.js @@ -0,0 +1,472 @@ +/** +* 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 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 + * 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. + */ +exports.makeTemplate = function(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: {}, + 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. + */ + delete template.layout.template; + 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 % oldTypeLen], typeTemplates[i]); + } + for(i = typeLen; 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; + + 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) { + 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); + } + } + } + } +} + +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 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 || attr._noTemplating || + attr.valType === 'data_array' || + (attr.arrayOk && Array.isArray(child)) + ) { + continue; + } + + if(!attr.valType && isPlainObject(child)) { + 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)) { + 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); + 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 { + 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; +} + +/** + * 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; + + 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 { + crawlLayoutForContainers(fullLayout, ['layout']); + crawlLayoutTemplateForContainers(layoutTemplate, 'layout'); + } + + 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 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/src/plot_api/validate.js b/src/plot_api/validate.js index ae789027bf5..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)}; @@ -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 [ @@ -390,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/src/plots/array_container_defaults.js b/src/plots/array_container_defaults.js index 70983dc33d8..47b17e48e09 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 * @@ -24,6 +25,9 @@ var Lib = require('../lib'); * 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 * @@ -32,8 +36,6 @@ var Lib = require('../lib'); * - itemOut {object} item in full layout * - parentObj {object} (as in closure) * - opts {object} (as in closure) - * - itemOpts {object} - * - itemIsNotPlainObject {boolean} * N.B. * * - opts is passed to handleItemDefaults so it can also store @@ -42,28 +44,40 @@ var Lib = require('../lib'); */ 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] : [], - contOut = parentObjOut[name] = [], - i; + var contIn = Lib.isArrayOrTypedArray(parentObjIn[name]) ? parentObjIn[name] : []; + var contOut = parentObjOut[name] = []; + var templater = Template.arrayTemplater(parentObjOut, name, inclusionAttr); + var i, itemOut; for(i = 0; i < contIn.length; i++) { - var itemIn = contIn[i], - itemOut = {}, - itemOpts = {}; + var itemIn = contIn[i]; if(!Lib.isPlainObject(itemIn)) { - itemOpts.itemIsNotPlainObject = true; - itemIn = {}; + itemOut = templater.newItem({}); + itemOut[inclusionAttr] = false; + } + else { + itemOut = templater.newItem(itemIn); } - opts.handleItemDefaults(itemIn, itemOut, parentObjOut, opts, itemOpts); - - itemOut._input = itemIn; itemOut._index = i; + if(itemOut[inclusionAttr] !== false) { + opts.handleItemDefaults(itemIn, itemOut, parentObjOut, opts); + } + + contOut.push(itemOut); + } + + var defaultItems = templater.defaultItems(); + for(i = 0; i < defaultItems.length; i++) { + itemOut = defaultItems[i]; + itemOut._index = contOut.length; + opts.handleItemDefaults({}, itemOut, parentObjOut, opts, {}); contOut.push(itemOut); } @@ -75,4 +89,6 @@ module.exports = function handleArrayContainerDefaults(parentObjIn, parentObjOut Lib.relinkPrivateKeys(contOut[i], previousContOut[i]); } } + + return contOut; }; 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/axes.js b/src/plots/cartesian/axes.js index 5bd190fe33d..38d56e912ca 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.enabled && 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.enabled && isProperLogStop(ax.dtick, stopi.dtickrange)) { + tickstop = stopi; break; } } 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/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 2fe31f5d4c8..33971a84d24 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'); @@ -60,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. + // 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.', 'By default, plotly attempts to determined the axis type', @@ -510,9 +516,17 @@ module.exports = { '*%H~%M~%S.%2f* would display *09~15~23.46*' ].join(' ') }, - tickformatstops: { - _isLinkedToArray: 'tickformatstop', - + tickformatstops: templatedArray('tickformatstop', { + enabled: { + 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', @@ -537,7 +551,7 @@ module.exports = { ].join(' ') }, editType: 'ticks' - }, + }), hoverformat: { valType: 'string', dflt: '', 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/cartesian/tick_label_defaults.js b/src/plots/cartesian/tick_label_defaults.js index 06d3212a36d..c899ff91def 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); @@ -26,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, @@ -37,7 +38,14 @@ 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', + inclusionAttr: 'enabled', + handleItemDefaults: tickformatstopDefaults + }); + } if(!tickFormat && axType !== 'date') { coerce('showexponent', showAttrDflt); coerce('exponentformat'); @@ -77,25 +85,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) { 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 enabled = coerce('enabled'); + if(enabled) { coerce('dtickrange'); coerce('value'); - - valuesOut.push(valueOut); } } 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/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/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..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,15 @@ module.exports = overrideAll({ ].join(' ') }, - layers: { - _isLinkedToArray: 'layer', - + layers: templatedArray('layer', { + visible: { + valType: 'boolean', + role: 'info', + dflt: true, + description: [ + 'Determines whether this layer is displayed' + ].join(' ') + }, sourcetype: { valType: 'enumerated', values: ['geojson', 'vector'], @@ -236,5 +243,5 @@ module.exports = overrideAll({ textfont: fontAttr, textposition: Lib.extendFlat({}, textposition, { arrayOk: false }) } - } + }) }, 'plot', 'from-root'); diff --git a/src/plots/mapbox/layout_defaults.js b/src/plots/mapbox/layout_defaults.js index 7c381dd9144..377dbea7281 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) { 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'); + 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/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_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.', 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/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(), 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/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/parcoords/defaults.js b/src/traces/parcoords/defaults.js index 07d1e97b0a8..e88b9f0b188 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) { 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)) { + 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/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'); 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) diff --git a/src/traces/splom/defaults.js b/src/traces/splom/defaults.js index 8e5f930d085..afcb002e8b3 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,16 @@ 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) { 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 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; - } - - traceOut._length = commonLength; + coerce('label'); + var values = coerce('values'); - return dimensionsOut.length; + if(!(values && values.length)) dimOut.visible = false; + else coerce('visible'); } function handleAxisDefaults(traceIn, traceOut, layout, coerce) { diff --git a/test/image/baselines/template.png b/test/image/baselines/template.png new file mode 100644 index 00000000000..baee368fbbf Binary files /dev/null and b/test/image/baselines/template.png differ 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/image/mocks/template.json b/test/image/mocks/template.json new file mode 100644 index 00000000000..6097406a41c --- /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}, "name": "new watermark"}, + {"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)"} + } + ] + } + } + } +} diff --git a/test/jasmine/tests/annotations_test.js b/test/jasmine/tests/annotations_test.js index 906e354654b..63109e4f94c 100644 --- a/test/jasmine/tests/annotations_test.js +++ b/test/jasmine/tests/annotations_test.js @@ -55,12 +55,10 @@ 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 - }); + _index: i + })); }); }); diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 5f306a750b9..7b6352a605e 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() { @@ -2814,14 +2820,17 @@ describe('Test Axes.getTickformat', function() { it('get proper tickformatstop for linear axis', function() { var lineartickformatstops = [ { + enabled: true, dtickrange: [null, 1], value: '.f2', }, { + enabled: true, dtickrange: [1, 100], value: '.f1', }, { + enabled: true, dtickrange: [100, null], value: 'g', } @@ -2848,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].enabled = 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() { @@ -2861,34 +2883,42 @@ describe('Test Axes.getTickformat', function() { var YEAR = 'M12'; // or 365.25 * DAY; var datetickformatstops = [ { + enabled: true, dtickrange: [null, SECOND], value: '%H:%M:%S.%L ms' // millisecond }, { + enabled: true, dtickrange: [SECOND, MINUTE], value: '%H:%M:%S s' // second }, { + enabled: true, dtickrange: [MINUTE, HOUR], value: '%H:%M m' // minute }, { + enabled: true, dtickrange: [HOUR, DAY], value: '%H:%M h' // hour }, { + enabled: true, dtickrange: [DAY, WEEK], value: '%e. %b d' // day }, { + enabled: true, dtickrange: [WEEK, MONTH], value: '%e. %b w' // week }, { + enabled: true, dtickrange: [MONTH, YEAR], value: '%b \'%y M' // month }, { + enabled: true, dtickrange: [YEAR, null], value: '%Y Y' // year } @@ -2939,18 +2969,22 @@ describe('Test Axes.getTickformat', function() { it('get proper tickformatstop for log axis', function() { var logtickformatstops = [ { + enabled: true, dtickrange: [null, 'L0.01'], value: '.f3', }, { + enabled: true, dtickrange: ['L0.01', 'L1'], value: '.f2', }, { + enabled: true, dtickrange: ['D1', 'D2'], value: '.f1', }, { + enabled: true, dtickrange: [1, null], value: 'g' } @@ -3084,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/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/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/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/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/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/range_selector_test.js b/test/jasmine/tests/range_selector_test.js index 16c59a23466..c63e17039cf 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() { @@ -55,24 +55,25 @@ 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() { 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 } + jasmine.objectContaining({ visible: true, step: 'year', stepmode: 'backward', count: 10, _index: 0 }), + jasmine.objectContaining({ visible: true, step: 'month', stepmode: 'backward', count: 6, _index: 1 }) ]); }); @@ -118,11 +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() { @@ -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/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..6e6764df1be 100644 --- a/test/jasmine/tests/shapes_test.js +++ b/test/jasmine/tests/shapes_test.js @@ -77,11 +77,10 @@ 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/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/template_test.js b/test/jasmine/tests/template_test.js new file mode 100644 index 00000000000..81ffe8f7e2f --- /dev/null +++ b/test/jasmine/tests/template_test.js @@ -0,0 +1,349 @@ +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 failTest = require('../assets/fail_test'); +var drag = require('../assets/drag'); + +var scatterFillMock = require('@mocks/scatter_fill_self_next.json'); +var templateMock = require('@mocks/template.json'); + + +describe('makeTemplate', function() { + it('does not template arrays', function() { + var template = Plotly.makeTemplate(Lib.extendDeep({}, scatterFillMock)); + expect(template).toEqual({ + data: {scatter: [ + {fill: 'tonext', line: {shape: 'spline'}}, + {fill: 'tonext'}, + {fill: 'toself'} + ] }, + layout: { + title: 'Fill toself and tonext', + width: 400, + height: 400 + } + }); + }); + + 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', size: [1, 2, 3]} + }]}; + var template = Plotly.makeTemplate(figure); + 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} + ] + }); + }); + + 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'} + } + }; + + 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.templateitemname).toBe('warning2'); + expect(schoolItem.x).toBeWithin(1, 0.001); + 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.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; + } + + 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); + }); +}); + +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.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.' + }, { + 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.layout', function() { + var noLayoutMock = Lib.extendDeep({}, cleanMock); + delete noLayoutMock.layout.template.layout; + + checkValidate(noLayoutMock, [{ + code: 'layout', + msg: 'The template has no key layout.' + }], 1); + }); + +}); 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/updatemenus_test.js b/test/jasmine/tests/updatemenus_test.js index 9b7d64e0361..af376c3eafb 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,13 @@ 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 +93,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 +107,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 +138,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 +167,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 +279,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 +289,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 +300,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 +321,9 @@ describe('update menus initialization', function() { {method: 'restyle', args: [], label: 'second'}, ] }] - }).then(done); + }) + .catch(failTest) + .then(done); }); afterEach(function() { @@ -335,7 +352,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 +438,7 @@ describe('update menus interactions', function() { assertPushMargins([false, false, false]); }) + .catch(failTest) .then(done); }); @@ -446,8 +466,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 +499,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 +516,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 +543,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 +565,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 +597,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 +645,7 @@ describe('update menus interactions', function() { .then(function() { assertActive(gd, [1]); }) + .catch(failTest) .then(done); }); @@ -629,9 +679,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 +734,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 +767,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 +792,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 +934,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 +1044,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() { diff --git a/test/jasmine/tests/validate_test.js b/test/jasmine/tests/validate_test.js index cd868ee9782..0c1074fd250 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() { @@ -526,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(); + }); });