diff --git a/src/components/colorbar/connect.js b/src/components/colorbar/connect.js new file mode 100644 index 00000000000..59ce044a7f2 --- /dev/null +++ b/src/components/colorbar/connect.js @@ -0,0 +1,67 @@ +/** +* 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 Colorscale = require('../colorscale'); +var drawColorbar = require('./draw'); + +/** + * connectColorbar: create a colorbar from a trace, using its module to + * describe the connection. + * + * @param {DOM element} gd + * + * @param {Array} cd + * calcdata entry for this trace. cd[0].trace is the trace itself, and the + * colorbar object will be stashed in cd[0].t.cb + * + * @param {object|function} moduleOpts + * may be a function(gd, cd) to override the standard handling below. If + * an object, should have these keys: + * @param {Optional(string)} moduleOpts.container + * name of the container inside the trace where the colorbar and colorscale + * attributes live (ie 'marker', 'line') - omit if they're at the trace root. + * @param {string} moduleOpts.min + * name of the attribute holding the value of the minimum color + * @param {string} moduleOpts.max + * name of the attribute holding the value of the maximum color + * @param {Optional(string)} moduleOpts.vals + * name of the attribute holding the (numeric) color data + * used only if min/max fail. May be omitted if these are always + * pre-calculated. + */ +module.exports = function connectColorbar(gd, cd, moduleOpts) { + if(typeof moduleOpts === 'function') return moduleOpts(gd, cd); + + var trace = cd[0].trace; + var cbId = 'cb' + trace.uid; + var containerName = moduleOpts.container; + var container = containerName ? trace[containerName] : trace; + + gd._fullLayout._infolayer.selectAll('.' + cbId).remove(); + if(!container || !container.showscale) return; + + var zmin = container[moduleOpts.min]; + var zmax = container[moduleOpts.max]; + + var cb = cd[0].t.cb = drawColorbar(gd, cbId); + var sclFunc = Colorscale.makeColorScaleFunc( + Colorscale.extractScale( + container.colorscale, + zmin, + zmax + ), + { noNumericCheck: true } + ); + + cb.fillcolor(sclFunc) + .filllevels({start: zmin, end: zmax, size: (zmax - zmin) / 254}) + .options(container.colorbar)(); +}; diff --git a/src/components/colorbar/draw.js b/src/components/colorbar/draw.js index 83ac58960f6..8614333873d 100644 --- a/src/components/colorbar/draw.js +++ b/src/components/colorbar/draw.js @@ -23,7 +23,10 @@ var Drawing = require('../drawing'); var Color = require('../color'); var Titles = require('../titles'); var svgTextUtils = require('../../lib/svg_text_utils'); -var LINE_SPACING = require('../../constants/alignment').LINE_SPACING; +var alignmentConstants = require('../../constants/alignment'); +var LINE_SPACING = alignmentConstants.LINE_SPACING; +var FROM_TL = alignmentConstants.FROM_TL; +var FROM_BR = alignmentConstants.FROM_BR; var handleAxisDefaults = require('../../plots/cartesian/axis_defaults'); var handleAxisPositionDefaults = require('../../plots/cartesian/position_defaults'); @@ -122,13 +125,13 @@ module.exports = function draw(gd, id) { // when the colorbar itself is pushing the margins. // but then the fractional size is calculated based on the // actual graph size, so that the axes will size correctly. - var originalPlotHeight = fullLayout.height - fullLayout.margin.t - fullLayout.margin.b, - originalPlotWidth = fullLayout.width - fullLayout.margin.l - fullLayout.margin.r, + var plotHeight = gs.h, + plotWidth = gs.w, thickPx = Math.round(opts.thickness * - (opts.thicknessmode === 'fraction' ? originalPlotWidth : 1)), + (opts.thicknessmode === 'fraction' ? plotWidth : 1)), thickFrac = thickPx / gs.w, lenPx = Math.round(opts.len * - (opts.lenmode === 'fraction' ? originalPlotHeight : 1)), + (opts.lenmode === 'fraction' ? plotHeight : 1)), lenFrac = lenPx / gs.h, xpadFrac = opts.xpad / gs.w, yExtraPx = (opts.borderwidth + opts.outlinewidth) / 2, @@ -447,12 +450,10 @@ module.exports = function draw(gd, id) { } function drawTitle(titleClass, titleOpts) { - var trace = getTrace(), - propName; - if(Registry.traceIs(trace, 'markerColorscale')) { - propName = 'marker.colorbar.title'; - } - else propName = 'colorbar.title'; + var trace = getTrace(); + var propName = 'colorbar.title'; + var containerName = trace._module.colorbar.container; + if(containerName) propName = containerName + '.' + propName; var dfltTitleOpts = { propContainer: cbAxisOut, @@ -539,14 +540,35 @@ module.exports = function draw(gd, id) { 'translate(' + (gs.l - xoffset) + ',' + gs.t + ')'); // auto margin adjustment - Plots.autoMargin(gd, id, { - x: opts.x, - y: opts.y, - l: outerwidth * ({right: 1, center: 0.5}[opts.xanchor] || 0), - r: outerwidth * ({left: 1, center: 0.5}[opts.xanchor] || 0), - t: outerheight * ({bottom: 1, middle: 0.5}[opts.yanchor] || 0), - b: outerheight * ({top: 1, middle: 0.5}[opts.yanchor] || 0) - }); + var marginOpts = {}; + var tFrac = FROM_TL[opts.yanchor]; + var bFrac = FROM_BR[opts.yanchor]; + if(opts.lenmode === 'pixels') { + marginOpts.y = opts.y; + marginOpts.t = outerheight * tFrac; + marginOpts.b = outerheight * bFrac; + } + else { + marginOpts.t = marginOpts.b = 0; + marginOpts.yt = opts.y + opts.len * tFrac; + marginOpts.yb = opts.y - opts.len * bFrac; + } + + var lFrac = FROM_TL[opts.xanchor]; + var rFrac = FROM_BR[opts.xanchor]; + if(opts.thicknessmode === 'pixels') { + marginOpts.x = opts.x; + marginOpts.l = outerwidth * lFrac; + marginOpts.r = outerwidth * rFrac; + } + else { + var extraThickness = outerwidth - thickPx; + marginOpts.l = extraThickness * lFrac; + marginOpts.r = extraThickness * rFrac; + marginOpts.xl = opts.x - opts.thickness * lFrac; + marginOpts.xr = opts.x + opts.thickness * rFrac; + } + Plots.autoMargin(gd, id, marginOpts); } var cbDone = Lib.syncOrAsync([ diff --git a/src/components/colorbar/index.js b/src/components/colorbar/index.js index 2f525b2b36c..54131bf2800 100644 --- a/src/components/colorbar/index.js +++ b/src/components/colorbar/index.js @@ -11,9 +11,7 @@ exports.attributes = require('./attributes'); - exports.supplyDefaults = require('./defaults'); - +exports.connect = require('./connect'); exports.draw = require('./draw'); - exports.hasColorbar = require('./has_colorbar'); diff --git a/src/components/rangeslider/draw.js b/src/components/rangeslider/draw.js index 079667bd7e2..e1d0b9f609c 100644 --- a/src/components/rangeslider/draw.js +++ b/src/components/rangeslider/draw.js @@ -61,15 +61,9 @@ module.exports = function(gd) { // remove exiting sliders and their corresponding clip paths rangeSliders.exit().each(function(axisOpts) { - var rangeSlider = d3.select(this), - opts = axisOpts[constants.name]; - - rangeSlider.remove(); + var opts = axisOpts[constants.name]; fullLayout._topdefs.select('#' + opts._clipId).remove(); - }); - - // remove push margin object(s) - if(rangeSliders.exit().size()) clearPushMargins(gd); + }).remove(); // return early if no range slider is visible if(rangeSliderData.length === 0) return; @@ -602,16 +596,3 @@ function drawGrabbers(rangeSlider, gd, axisOpts, opts) { }); grabAreaMax.attr('height', opts._height); } - -function clearPushMargins(gd) { - var pushMargins = gd._fullLayout._pushmargin || {}, - keys = Object.keys(pushMargins); - - for(var i = 0; i < keys.length; i++) { - var k = keys[i]; - - if(k.indexOf(constants.name) !== -1) { - Plots.autoMargin(gd, k); - } - } -} diff --git a/src/components/sliders/draw.js b/src/components/sliders/draw.js index 15d60bacbe5..512f7fef9fc 100644 --- a/src/components/sliders/draw.js +++ b/src/components/sliders/draw.js @@ -36,10 +36,23 @@ module.exports = function draw(gd) { .classed(constants.containerClassName, true) .style('cursor', 'ew-resize'); - sliders.exit().remove(); + function clearSlider(sliderOpts) { + if(sliderOpts._commandObserver) { + sliderOpts._commandObserver.remove(); + delete sliderOpts._commandObserver; + } + + // Most components don't need to explicitly remove autoMargin, because + // marginPushers does this - but slider updates don't go through + // a full replot so we need to explicitly remove it. + Plots.autoMargin(gd, autoMarginId(sliderOpts)); + } - // If no more sliders, clear the margisn: - if(sliders.exit().size()) clearPushMargins(gd); + sliders.exit().each(function() { + d3.select(this).selectAll('g.' + constants.groupClassName) + .each(clearSlider); + }) + .remove(); // Return early if no menus visible: if(sliderData.length === 0) return; @@ -50,14 +63,9 @@ module.exports = function draw(gd) { sliderGroups.enter().append('g') .classed(constants.groupClassName, true); - sliderGroups.exit().each(function(sliderOpts) { - d3.select(this).remove(); - - sliderOpts._commandObserver.remove(); - delete sliderOpts._commandObserver; - - Plots.autoMargin(gd, constants.autoMarginIdRoot + sliderOpts._index); - }); + sliderGroups.exit() + .each(clearSlider) + .remove(); // Find the dimensions of the sliders: for(var i = 0; i < sliderData.length; i++) { @@ -92,6 +100,10 @@ module.exports = function draw(gd) { }); }; +function autoMarginId(sliderOpts) { + return constants.autoMarginIdRoot + sliderOpts._index; +} + // This really only just filters by visibility: function makeSliderData(fullLayout, gd) { var contOpts = fullLayout[constants.name], @@ -221,14 +233,25 @@ function findDimensions(gd, sliderOpts) { dims.lx = Math.round(dims.lx); dims.ly = Math.round(dims.ly); - Plots.autoMargin(gd, constants.autoMarginIdRoot + sliderOpts._index, { - x: sliderOpts.x, + var marginOpts = { y: sliderOpts.y, - l: dims.outerLength * FROM_TL[xanchor], - r: dims.outerLength * FROM_BR[xanchor], b: dims.height * FROM_BR[yanchor], t: dims.height * FROM_TL[yanchor] - }); + }; + + if(sliderOpts.lenmode === 'fraction') { + marginOpts.l = 0; + marginOpts.xl = sliderOpts.x - sliderOpts.len * FROM_TL[xanchor]; + marginOpts.r = 0; + marginOpts.xr = sliderOpts.x + sliderOpts.len * FROM_BR[xanchor]; + } + else { + marginOpts.x = sliderOpts.x; + marginOpts.l = dims.outerLength * FROM_TL[xanchor]; + marginOpts.r = dims.outerLength * FROM_BR[xanchor]; + } + + Plots.autoMargin(gd, autoMarginId(sliderOpts), marginOpts); } function drawSlider(gd, sliderGroup, sliderOpts) { @@ -594,16 +617,3 @@ function drawRail(sliderGroup, sliderOpts) { (dims.inputAreaWidth - constants.railWidth) * 0.5 + dims.currentValueTotalHeight ); } - -function clearPushMargins(gd) { - var pushMargins = gd._fullLayout._pushmargin || {}, - keys = Object.keys(pushMargins); - - for(var i = 0; i < keys.length; i++) { - var k = keys[i]; - - if(k.indexOf(constants.autoMarginIdRoot) !== -1) { - Plots.autoMargin(gd, k); - } - } -} diff --git a/src/components/updatemenus/draw.js b/src/components/updatemenus/draw.js index faee1578a6e..e72aa49231e 100644 --- a/src/components/updatemenus/draw.js +++ b/src/components/updatemenus/draw.js @@ -54,6 +54,10 @@ module.exports = function draw(gd) { * ... */ + function clearAutoMargin(menuOpts) { + Plots.autoMargin(gd, autoMarginId(menuOpts)); + } + // draw update menu container var menus = fullLayout._menulayer .selectAll('g.' + constants.containerClassName) @@ -63,10 +67,15 @@ module.exports = function draw(gd) { .classed(constants.containerClassName, true) .style('cursor', 'pointer'); - menus.exit().remove(); - - // remove push margin object(s) - if(menus.exit().size()) clearPushMargins(gd); + menus.exit().each(function() { + // Most components don't need to explicitly remove autoMargin, because + // marginPushers does this - but updatemenu updates don't go through + // a full replot so we need to explicitly remove it. + // This is for removing *all* updatemenus, removing individuals is + // handled below, in headerGroups.exit + d3.select(this).selectAll('g.' + constants.headerGroupClassName) + .each(clearAutoMargin); + }).remove(); // return early if no update menus are visible if(menuData.length === 0) return; @@ -97,21 +106,13 @@ module.exports = function draw(gd) { if(headerGroups.enter().size()) { // make sure gButton is on top of all headers gButton.node().parentNode.appendChild(gButton.node()); - - gButton - .call(removeAllButtons) - .attr(constants.menuIndexAttrName, '-1'); + gButton.call(removeAllButtons); } headerGroups.exit().each(function(menuOpts) { - d3.select(this).remove(); - - gButton - .call(removeAllButtons) - .attr(constants.menuIndexAttrName, '-1'); - - Plots.autoMargin(gd, constants.autoMarginIdRoot + menuOpts._index); - }); + gButton.call(removeAllButtons); + clearAutoMargin(menuOpts); + }).remove(); // draw headers! headerGroups.each(function(menuOpts) { @@ -219,16 +220,8 @@ function drawHeader(gd, gHeader, gButton, scrollBox, menuOpts) { }); header.on('click', function() { - gButton.call(removeAllButtons); - - - // if this menu is active, fold the dropdown container - // otherwise, make this menu active - gButton.attr( - constants.menuIndexAttrName, - isActive(gButton, menuOpts) ? - -1 : - String(menuOpts._index) + gButton.call(removeAllButtons, + String(isActive(gButton, menuOpts) ? -1 : menuOpts._index) ); drawButtons(gd, gHeader, gButton, scrollBox, menuOpts); @@ -608,7 +601,7 @@ function findDimensions(gd, menuOpts) { dims.lx = Math.round(dims.lx); dims.ly = Math.round(dims.ly); - Plots.autoMargin(gd, constants.autoMarginIdRoot + menuOpts._index, { + Plots.autoMargin(gd, autoMarginId(menuOpts), { x: menuOpts.x, y: menuOpts.y, l: paddedWidth * ({right: 1, center: 0.5}[xanchor] || 0), @@ -618,6 +611,10 @@ function findDimensions(gd, menuOpts) { }); } +function autoMarginId(menuOpts) { + return constants.autoMarginIdRoot + menuOpts._index; +} + // set item positions (mutates posOpts) function setItemPosition(item, menuOpts, posOpts, overrideOpts) { overrideOpts = overrideOpts || {}; @@ -655,19 +652,8 @@ function setItemPosition(item, menuOpts, posOpts, overrideOpts) { posOpts.index++; } -function removeAllButtons(gButton) { - gButton.selectAll('g.' + constants.dropdownButtonClassName).remove(); -} - -function clearPushMargins(gd) { - var pushMargins = gd._fullLayout._pushmargin || {}; - var keys = Object.keys(pushMargins); - - for(var i = 0; i < keys.length; i++) { - var k = keys[i]; - - if(k.indexOf(constants.autoMarginIdRoot) !== -1) { - Plots.autoMargin(gd, k); - } - } +function removeAllButtons(gButton, newMenuIndexAttr) { + gButton + .attr(constants.menuIndexAttrName, newMenuIndexAttr || '-1') + .selectAll('g.' + constants.dropdownButtonClassName).remove(); } diff --git a/src/lib/index.js b/src/lib/index.js index 8b9d36d1e11..6e0a3e24a61 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -217,21 +217,24 @@ lib.simpleMap = function(array, func, x1, x2) { return out; }; -// random string generator -lib.randstr = function randstr(existing, bits, base) { - /* - * Include number of bits, the base of the string you want - * and an optional array of existing strings to avoid. - */ +/** + * Random string generator + * + * @param {object} existing + * pass in strings to avoid as keys with truthy values + * @param {int} bits + * bits of information in the output string, default 24 + * @param {int} base + * base of string representation, default 16. Should be a power of 2. + */ +lib.randstr = function randstr(existing, bits, base, _recursion) { if(!base) base = 16; if(bits === undefined) bits = 24; if(bits <= 0) return '0'; - var digits = Math.log(Math.pow(2, bits)) / Math.log(base), - res = '', - i, - b, - x; + var digits = Math.log(Math.pow(2, bits)) / Math.log(base); + var res = ''; + var i, b, x; for(i = 2; digits === Infinity; i *= 2) { digits = Math.log(Math.pow(2, bits / i)) / Math.log(base) * i; @@ -251,9 +254,13 @@ lib.randstr = function randstr(existing, bits, base) { } var parsed = parseInt(res, base); - if((existing && (existing.indexOf(res) > -1)) || + if((existing && existing[res]) || (parsed !== Infinity && parsed >= Math.pow(2, bits))) { - return randstr(existing, bits, base); + if(_recursion > 10) { + lib.warn('randstr failed uniqueness'); + return res; + } + return randstr(existing, bits, base, (_recursion || 0) + 1); } else return res; }; diff --git a/src/lib/svg_text_utils.js b/src/lib/svg_text_utils.js index 6f08f91371b..fc3f718db16 100644 --- a/src/lib/svg_text_utils.js +++ b/src/lib/svg_text_utils.js @@ -164,7 +164,7 @@ function cleanEscapesForTex(s) { } function texToSVG(_texString, _config, _callback) { - var randomID = 'math-output-' + Lib.randstr([], 64); + var randomID = 'math-output-' + Lib.randstr({}, 64); var tmpDiv = d3.select('body').append('div') .attr({id: randomID}) .style({visibility: 'hidden', position: 'absolute'}) diff --git a/src/plot_api/edit_types.js b/src/plot_api/edit_types.js index ea6defdc2c5..ab9ed4ed84b 100644 --- a/src/plot_api/edit_types.js +++ b/src/plot_api/edit_types.js @@ -35,7 +35,7 @@ var layoutOpts = { valType: 'flaglist', extras: ['none'], flags: [ - 'calc', 'calcIfAutorange', 'plot', 'legend', 'ticks', 'axrange', 'margins', + 'calc', 'calcIfAutorange', 'plot', 'legend', 'ticks', 'axrange', 'layoutstyle', 'modebar', 'camera', 'arraydraw' ], description: [ @@ -47,7 +47,6 @@ var layoutOpts = { '*plot* calls `Plotly.plot` but without first clearing `gd.calcdata`.', '*legend* only redraws the legend.', '*ticks* only redraws axis ticks, labels, and gridlines.', - '*margins* recomputes ticklabel automargins.', '*axrange* minimal sequence when updating axis ranges.', '*layoutstyle* reapplies global and SVG cartesian axis styles.', '*modebar* just updates the modebar.', diff --git a/src/plot_api/helpers.js b/src/plot_api/helpers.js index b0912b31020..8a49ae911ae 100644 --- a/src/plot_api/helpers.js +++ b/src/plot_api/helpers.js @@ -197,42 +197,17 @@ function cleanAxRef(container, attr) { } /* - * cleanData: Make a few changes to the data right away - * before it gets used for anything - * Mostly for backward compatibility, modifies the data traces users provide. + * cleanData: Make a few changes to the data for backward compatibility + * before it gets used for anything. Modifies the data traces users provide. * * Important: if you're going to add something here that modifies a data array, * update it in place so the new array === the old one. */ -exports.cleanData = function(data, existingData) { - // Enforce unique IDs - var suids = [], // seen uids --- so we can weed out incoming repeats - uids = data.concat(Array.isArray(existingData) ? existingData : []) - .filter(function(trace) { return 'uid' in trace; }) - .map(function(trace) { return trace.uid; }); - +exports.cleanData = function(data) { for(var tracei = 0; tracei < data.length; tracei++) { var trace = data[tracei]; var i; - // assign uids to each trace and detect collisions. - if(!('uid' in trace) || suids.indexOf(trace.uid) !== -1) { - var newUid; - - for(i = 0; i < 100; i++) { - newUid = Lib.randstr(uids); - if(suids.indexOf(newUid) === -1) break; - } - trace.uid = Lib.randstr(uids); - uids.push(trace.uid); - } - // keep track of already seen uids, so that if there are - // doubles we force the trace with a repeat uid to - // acquire a new one - suids.push(trace.uid); - - // BACKWARD COMPATIBILITY FIXES - // use xbins to bin data in x, and ybins to bin data in y if(trace.type === 'histogramy' && 'xbins' in trace && !('ybins' in trace)) { trace.ybins = trace.xbins; @@ -299,14 +274,14 @@ exports.cleanData = function(data, existingData) { } // fix typo in colorscale definition - if(Registry.traceIs(trace, '2dMap')) { - if(trace.colorscale === 'YIGnBu') trace.colorscale = 'YlGnBu'; - if(trace.colorscale === 'YIOrRd') trace.colorscale = 'YlOrRd'; - } - if(Registry.traceIs(trace, 'markerColorscale') && trace.marker) { - var cont = trace.marker; - if(cont.colorscale === 'YIGnBu') cont.colorscale = 'YlGnBu'; - if(cont.colorscale === 'YIOrRd') cont.colorscale = 'YlOrRd'; + var _module = Registry.getModule(trace); + if(_module && _module.colorbar) { + var containerName = _module.colorbar.container; + var container = containerName ? trace[containerName] : trace; + if(container && container.colorscale) { + if(container.colorscale === 'YIGnBu') container.colorscale = 'YlGnBu'; + if(container.colorscale === 'YIOrRd') container.colorscale = 'YlOrRd'; + } } // fix typo in surface 'highlight*' definitions @@ -619,12 +594,3 @@ exports.clearAxisTypes = function(gd, traces, layoutUpdate) { } } }; - -exports.clearAxisAutomargins = function(gd) { - var keys = Object.keys(gd._fullLayout._pushmargin); - for(var i = 0; i < keys.length; i++) { - if(keys[i].indexOf('automargin') !== -1) { - delete gd._fullLayout._pushmargin[keys[i]]; - } - } -}; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 6d23574423f..e00e3acc045 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -26,6 +26,7 @@ var Polar = require('../plots/polar/legacy'); var Axes = require('../plots/cartesian/axes'); var Drawing = require('../components/drawing'); var Color = require('../components/color'); +var connectColorbar = require('../components/colorbar/connect'); var initInteractions = require('../plots/cartesian/graph_interact').initInteractions; var xmlnsNamespaces = require('../constants/xmlns_namespaces'); var svgTextUtils = require('../lib/svg_text_utils'); @@ -123,7 +124,7 @@ exports.plot = function(gd, data, layout, config) { // if there is already data on the graph, append the new data // if you only want to redraw, pass a non-array for data if(Array.isArray(data)) { - helpers.cleanData(data, gd.data); + helpers.cleanData(data); if(graphWasEmpty) gd.data = data; else gd.data.push.apply(gd.data, data); @@ -252,18 +253,24 @@ exports.plot = function(gd, data, layout, config) { var calcdata = gd.calcdata; var i, cd, trace; - Registry.getComponentMethod('legend', 'draw')(gd); - Registry.getComponentMethod('rangeselector', 'draw')(gd); - Registry.getComponentMethod('sliders', 'draw')(gd); - Registry.getComponentMethod('updatemenus', 'draw')(gd); + // First reset the list of things that are allowed to change the margins + // So any deleted traces or components will be wiped out of the + // automargin calculation. + // This means *every* margin pusher must be listed here, even if it + // doesn't actually try to push the margins until later. + Plots.clearAutoMarginIds(gd); + + subroutines.drawMarginPushers(gd); + Axes.allowAutoMargin(gd); for(i = 0; i < calcdata.length; i++) { cd = calcdata[i]; trace = cd[0].trace; - if(trace.visible !== true || !trace._module.colorbar) { + var colorbarOpts = trace._module.colorbar; + if(trace.visible !== true || !colorbarOpts) { Plots.autoMargin(gd, 'cb' + trace.uid); } - else trace._module.colorbar(gd, cd); + else connectColorbar(gd, cd, colorbarOpts); } Plots.doAutoMargin(gd); @@ -354,6 +361,11 @@ exports.plot = function(gd, data, layout, config) { initInteractions, Plots.addLinks, Plots.rehover, + // TODO: doAutoMargin is only needed here for axis automargin, which + // happens outside of marginPushers where all the other automargins are + // calculated. Would be much better to separate margin calculations from + // component drawing - see https://github.com/plotly/plotly.js/issues/2704 + Plots.doAutoMargin, Plots.previousPromises ); @@ -559,7 +571,7 @@ exports.redraw = function(gd) { throw new Error('This element is not a Plotly plot: ' + gd); } - helpers.cleanData(gd.data, gd.data); + helpers.cleanData(gd.data); helpers.cleanLayout(gd.layout); gd.calcdata = undefined; @@ -1060,7 +1072,7 @@ exports.addTraces = function addTraces(gd, traces, newIndices) { return Lib.extendFlat({}, trace); }); - helpers.cleanData(traces, gd.data); + helpers.cleanData(traces); // add the traces to gd.data (no redrawing yet!) for(i = 0; i < traces.length; i++) { @@ -1439,6 +1451,12 @@ function _restyle(gd, aobj, traces) { if(newVal === undefined) continue; + var finalPart = param.parts[param.parts.length - 1]; + var prefix = ai.substr(0, ai.length - finalPart.length - 1); + var prefixDot = prefix ? prefix + '.' : ''; + var innerContFull = prefix ? + Lib.nestedProperty(contFull, prefix).get() : contFull; + valObject = PlotSchema.getTraceValObject(contFull, param.parts); if(valObject && valObject.impliedEdits && newVal !== null) { @@ -1452,31 +1470,27 @@ function _restyle(gd, aobj, traces) { // note that colorbar fractional sizing is based on the // original plot size, before anything (like a colorbar) // increases the margins - else if(ai === 'colorbar.thicknessmode' && param.get() !== newVal && - ['fraction', 'pixels'].indexOf(newVal) !== -1 && - contFull.colorbar) { - var thicknorm = - ['top', 'bottom'].indexOf(contFull.colorbar.orient) !== -1 ? - (fullLayout.height - fullLayout.margin.t - fullLayout.margin.b) : - (fullLayout.width - fullLayout.margin.l - fullLayout.margin.r); - doextra('colorbar.thickness', contFull.colorbar.thickness * - (newVal === 'fraction' ? 1 / thicknorm : thicknorm), i); - } - else if(ai === 'colorbar.lenmode' && param.get() !== newVal && - ['fraction', 'pixels'].indexOf(newVal) !== -1 && - contFull.colorbar) { - var lennorm = - ['top', 'bottom'].indexOf(contFull.colorbar.orient) !== -1 ? - (fullLayout.width - fullLayout.margin.l - fullLayout.margin.r) : - (fullLayout.height - fullLayout.margin.t - fullLayout.margin.b); - doextra('colorbar.len', contFull.colorbar.len * - (newVal === 'fraction' ? 1 / lennorm : lennorm), i); - } - else if(ai === 'colorbar.tick0' || ai === 'colorbar.dtick') { - doextra('colorbar.tickmode', 'linear', i); + else if((finalPart === 'thicknessmode' || finalPart === 'lenmode') && + oldVal !== newVal && + (newVal === 'fraction' || newVal === 'pixels') && + innerContFull + ) { + var gs = fullLayout._size; + var orient = innerContFull.orient; + var topOrBottom = (orient === 'top') || (orient === 'bottom'); + if(finalPart === 'thicknessmode') { + var thicknorm = topOrBottom ? gs.h : gs.w; + doextra(prefixDot + 'thickness', innerContFull.thickness * + (newVal === 'fraction' ? 1 / thicknorm : thicknorm), i); + } + else { + var lennorm = topOrBottom ? gs.w : gs.h; + doextra(prefixDot + 'len', innerContFull.len * + (newVal === 'fraction' ? 1 / lennorm : lennorm), i); + } } - if(ai === 'type' && (newVal === 'pie') !== (oldVal === 'pie')) { + else if(ai === 'type' && (newVal === 'pie') !== (oldVal === 'pie')) { var labelsTo = 'x', valuesTo = 'y'; if((newVal === 'bar' || oldVal === 'bar') && cont.orientation === 'h') { @@ -1666,7 +1680,6 @@ exports.relayout = function relayout(gd, astr, val) { // clear calcdata if required if(flags.calc) gd.calcdata = undefined; - if(flags.margins) helpers.clearAxisAutomargins(gd); // fill in redraw sequence @@ -1685,16 +1698,7 @@ exports.relayout = function relayout(gd, astr, val) { if(flags.layoutstyle) seq.push(subroutines.layoutStyles); if(flags.axrange) { - // N.B. leave as sequence of subroutines (for now) instead of - // subroutine of its own so that finalDraw always gets - // executed after drawData - seq.push( - function(gd) { - return subroutines.doTicksRelayout(gd, specs.rangesAltered); - }, - subroutines.drawData, - subroutines.finalDraw - ); + addAxRangeSequence(seq, specs.rangesAltered); } if(flags.ticks) seq.push(subroutines.doTicksRelayout); @@ -1718,6 +1722,21 @@ exports.relayout = function relayout(gd, astr, val) { }); }; +function addAxRangeSequence(seq, rangesAltered) { + // N.B. leave as sequence of subroutines (for now) instead of + // subroutine of its own so that finalDraw always gets + // executed after drawData + var doTicks = rangesAltered ? + function(gd) { return subroutines.doTicksRelayout(gd, rangesAltered); } : + subroutines.doTicksRelayout; + + seq.push( + doTicks, + subroutines.drawData, + subroutines.finalDraw + ); +} + var AX_RANGE_RE = /^[xyz]axis[0-9]*\.range(\[[0|1]\])?$/; var AX_AUTORANGE_RE = /^[xyz]axis[0-9]*\.autorange$/; var AX_DOMAIN_RE = /^[xyz]axis[0-9]*\.domain(\[[0|1]\])?$/; @@ -2137,7 +2156,6 @@ exports.update = function update(gd, traceUpdate, layoutUpdate, _traces) { // clear calcdata and/or axis types if required if(restyleFlags.clearCalc || relayoutFlags.calc) gd.calcdata = undefined; if(restyleFlags.clearAxisTypes) helpers.clearAxisTypes(gd, traces, layoutUpdate); - if(relayoutFlags.margins) helpers.clearAxisAutomargins(gd); // fill in redraw sequence var seq = []; @@ -2168,13 +2186,7 @@ exports.update = function update(gd, traceUpdate, layoutUpdate, _traces) { if(relayoutFlags.legend) seq.push(subroutines.doLegend); if(relayoutFlags.layoutstyle) seq.push(subroutines.layoutStyles); if(relayoutFlags.axrange) { - seq.push( - function(gd) { - return subroutines.doTicksRelayout(gd, relayoutSpecs.rangesAltered); - }, - subroutines.drawData, - subroutines.finalDraw - ); + addAxRangeSequence(seq, relayoutSpecs.rangesAltered); } if(relayoutFlags.ticks) seq.push(subroutines.doTicksRelayout); if(relayoutFlags.modebar) seq.push(subroutines.doModeBar); @@ -2259,7 +2271,7 @@ exports.react = function(gd, data, layout, config) { } gd.data = data || []; - helpers.cleanData(gd.data, []); + helpers.cleanData(gd.data); gd.layout = layout || {}; helpers.cleanLayout(gd.layout); @@ -2291,8 +2303,6 @@ exports.react = function(gd, data, layout, config) { // otherwise do the calcdata updates and calcTransform array remaps that we skipped earlier else Plots.supplyDefaultsUpdateCalc(gd.calcdata, newFullData); - if(relayoutFlags.margins) helpers.clearAxisAutomargins(gd); - // Note: what restyle/relayout use impliedEdits and clearAxisTypes for // must be handled by the user when using Plotly.react. @@ -2334,13 +2344,7 @@ exports.react = function(gd, data, layout, config) { if(restyleFlags.colorbars) seq.push(subroutines.doColorBars); if(relayoutFlags.legend) seq.push(subroutines.doLegend); if(relayoutFlags.layoutstyle) seq.push(subroutines.layoutStyles); - if(relayoutFlags.axrange) { - seq.push( - subroutines.doTicksRelayout, - subroutines.drawData, - subroutines.finalDraw - ); - } + if(relayoutFlags.axrange) addAxRangeSequence(seq); if(relayoutFlags.ticks) seq.push(subroutines.doTicksRelayout); if(relayoutFlags.modebar) seq.push(subroutines.doModeBar); if(relayoutFlags.camera) seq.push(subroutines.doCamera); @@ -3257,9 +3261,9 @@ function makePlotFramework(gd) { .classed('main-svg', true); if(!fullLayout._uid) { - var otherUids = []; + var otherUids = {}; d3.selectAll('defs').each(function() { - if(this.id) otherUids.push(this.id.split('-')[1]); + if(this.id) otherUids[this.id.split('-')[1]] = 1; }); fullLayout._uid = Lib.randstr(otherUids); } diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index c27788542af..252d78250e7 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -473,10 +473,10 @@ exports.doColorBars = function(gd) { cb._opts.line.color : trace.line.color }); } - if(Registry.traceIs(trace, 'markerColorscale')) { - cb.options(trace.marker.colorbar)(); - } - else cb.options(trace.colorbar)(); + var moduleOpts = trace._module.colorbar; + var containerName = moduleOpts.container; + var opts = (containerName ? trace[containerName] : trace).colorbar; + cb.options(opts)(); } } @@ -589,8 +589,19 @@ exports.finalDraw = function(gd) { Registry.getComponentMethod('shapes', 'draw')(gd); Registry.getComponentMethod('images', 'draw')(gd); Registry.getComponentMethod('annotations', 'draw')(gd); - Registry.getComponentMethod('legend', 'draw')(gd); + // TODO: rangesliders really belong in marginPushers but they need to be + // drawn after data - can we at least get the margin pushing part separated + // out and done earlier? Registry.getComponentMethod('rangeslider', 'draw')(gd); + // TODO: rangeselector only needs to be here (in addition to drawMarginPushers) + // because the margins need to be fully determined before we can call + // autorange and update axis ranges (which rangeselector needs to know which + // button is active). Can we break out its automargin step from its draw step? + Registry.getComponentMethod('rangeselector', 'draw')(gd); +}; + +exports.drawMarginPushers = function(gd) { + Registry.getComponentMethod('legend', 'draw')(gd); Registry.getComponentMethod('rangeselector', 'draw')(gd); Registry.getComponentMethod('sliders', 'draw')(gd); Registry.getComponentMethod('updatemenus', 'draw')(gd); diff --git a/src/plots/attributes.js b/src/plots/attributes.js index 7c1dfa445b8..d00f27d2c2b 100644 --- a/src/plots/attributes.js +++ b/src/plots/attributes.js @@ -73,8 +73,7 @@ module.exports = { uid: { valType: 'string', role: 'info', - dflt: '', - editType: 'calc' + editType: 'plot' }, ids: { valType: 'data_array', diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 091b2b5df03..5bd190fe33d 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -2007,8 +2007,12 @@ axes.doTicksSingle = function(gd, arg, skipTitle) { } function doAutoMargins() { - if(!ax.automargin) { return; } + var pushKey = ax._name + '.automargin'; if(axLetter !== 'x' && axLetter !== 'y') { return; } + if(!ax.automargin) { + Plots.autoMargin(gd, pushKey); + return; + } var s = ax.side[0]; var push = {x: 0, y: 0, r: 0, l: 0, t: 0, b: 0}; @@ -2028,11 +2032,7 @@ axes.doTicksSingle = function(gd, arg, skipTitle) { push[s] += ax.titlefont.size; } - var pushKey = ax._name + '.automargin'; - var prevPush = fullLayout._pushmargin[pushKey]; - if(!prevPush || prevPush[s].size < push[s]) { - Plots.autoMargin(gd, pushKey, push); - } + Plots.autoMargin(gd, pushKey, push); } var done = Lib.syncOrAsync([ @@ -2255,6 +2255,28 @@ axes.doTicksSingle = function(gd, arg, skipTitle) { } }; +/** + * Find all margin pushers for 2D axes and reserve them for later use + * Both label and rangeslider automargin calculations happen later so + * we need to explicitly allow their ids in order to not delete them. + * + * TODO: can we pull the actual automargin calls forward to avoid this hack? + * We're probably also doing multiple redraws in this case, would be faster + * if we can just do the whole calculation ahead of time and draw once. + */ +axes.allowAutoMargin = function(gd) { + var axList = axes.list(gd, '', true); + for(var i = 0; i < axList.length; i++) { + var ax = axList[i]; + if(ax.automargin) { + Plots.allowAutoMargin(gd, ax._name + '.automargin'); + } + if(ax.rangeslider && ax.rangeslider.visible) { + Plots.allowAutoMargin(gd, 'rangeslider' + ax._id); + } + } +}; + // swap all the presentation attributes of the axes showing these traces axes.swap = function(gd, traces) { var axGroups = makeAxisGroups(gd, traces); diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 25bfda44393..2fe31f5d4c8 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -42,11 +42,11 @@ module.exports = { title: { valType: 'string', role: 'info', - editType: 'ticks+margins', + editType: 'ticks', description: 'Sets the title of this axis.' }, titlefont: fontAttrs({ - editType: 'ticks+margins', + editType: 'ticks', description: [ 'Sets this axis\' title font.' ].join(' ') @@ -100,10 +100,10 @@ module.exports = { valType: 'info_array', role: 'info', items: [ - {valType: 'any', editType: 'axrange+margins', impliedEdits: {'^autorange': false}}, - {valType: 'any', editType: 'axrange+margins', impliedEdits: {'^autorange': false}} + {valType: 'any', editType: 'axrange', impliedEdits: {'^autorange': false}}, + {valType: 'any', editType: 'axrange', impliedEdits: {'^autorange': false}} ], - editType: 'axrange+margins', + editType: 'axrange', impliedEdits: {'autorange': false}, description: [ 'Sets the range of this axis.', @@ -198,7 +198,7 @@ module.exports = { valType: 'enumerated', values: ['auto', 'linear', 'array'], role: 'info', - editType: 'ticks+margins', + editType: 'ticks', impliedEdits: {tick0: undefined, dtick: undefined}, description: [ 'Sets the tick mode for this axis.', @@ -216,7 +216,7 @@ module.exports = { min: 0, dflt: 0, role: 'style', - editType: 'ticks+margins', + editType: 'ticks', description: [ 'Specifies the maximum number of ticks for the particular axis.', 'The actual number of ticks will be chosen automatically to be', @@ -227,7 +227,7 @@ module.exports = { tick0: { valType: 'any', role: 'style', - editType: 'ticks+margins', + editType: 'ticks', impliedEdits: {tickmode: 'linear'}, description: [ 'Sets the placement of the first tick on this axis.', @@ -243,7 +243,7 @@ module.exports = { dtick: { valType: 'any', role: 'style', - editType: 'ticks+margins', + editType: 'ticks', impliedEdits: {tickmode: 'linear'}, description: [ 'Sets the step in-between ticks on this axis. Use with `tick0`.', @@ -269,7 +269,7 @@ module.exports = { }, tickvals: { valType: 'data_array', - editType: 'ticks+margins', + editType: 'ticks', description: [ 'Sets the values at which ticks on this axis appear.', 'Only has an effect if `tickmode` is set to *array*.', @@ -278,7 +278,7 @@ module.exports = { }, ticktext: { valType: 'data_array', - editType: 'ticks+margins', + editType: 'ticks', description: [ 'Sets the text displayed at the ticks position via `tickvals`.', 'Only has an effect if `tickmode` is set to *array*.', @@ -289,7 +289,7 @@ module.exports = { valType: 'enumerated', values: ['outside', 'inside', ''], role: 'style', - editType: 'ticks+margins', + editType: 'ticks', description: [ 'Determines whether ticks are drawn or not.', 'If **, this axis\' ticks are not drawn.', @@ -341,14 +341,14 @@ module.exports = { valType: 'boolean', dflt: true, role: 'style', - editType: 'ticks+margins', + editType: 'ticks', description: 'Determines whether or not the tick labels are drawn.' }, automargin: { valType: 'boolean', dflt: false, role: 'style', - editType: 'ticks+margins', + editType: 'ticks', description: [ 'Determines whether long tick labels automatically grow the figure', 'margins.' @@ -406,14 +406,14 @@ module.exports = { description: 'Determines whether spikelines are stuck to the cursor or to the closest datapoints.' }, tickfont: fontAttrs({ - editType: 'ticks+margins', + editType: 'ticks', description: 'Sets the tick font.' }), tickangle: { valType: 'angle', dflt: 'auto', role: 'style', - editType: 'ticks+margins', + editType: 'ticks', description: [ 'Sets the angle of the tick labels with respect to the horizontal.', 'For example, a `tickangle` of -90 draws the tick labels', @@ -424,7 +424,7 @@ module.exports = { valType: 'string', dflt: '', role: 'style', - editType: 'ticks+margins', + editType: 'ticks', description: 'Sets a tick label prefix.' }, showtickprefix: { @@ -432,7 +432,7 @@ module.exports = { values: ['all', 'first', 'last', 'none'], dflt: 'all', role: 'style', - editType: 'ticks+margins', + editType: 'ticks', description: [ 'If *all*, all tick labels are displayed with a prefix.', 'If *first*, only the first tick is displayed with a prefix.', @@ -444,7 +444,7 @@ module.exports = { valType: 'string', dflt: '', role: 'style', - editType: 'ticks+margins', + editType: 'ticks', description: 'Sets a tick label suffix.' }, showticksuffix: { @@ -452,7 +452,7 @@ module.exports = { values: ['all', 'first', 'last', 'none'], dflt: 'all', role: 'style', - editType: 'ticks+margins', + editType: 'ticks', description: 'Same as `showtickprefix` but for tick suffixes.' }, showexponent: { @@ -460,7 +460,7 @@ module.exports = { values: ['all', 'first', 'last', 'none'], dflt: 'all', role: 'style', - editType: 'ticks+margins', + editType: 'ticks', description: [ 'If *all*, all exponents are shown besides their significands.', 'If *first*, only the exponent of the first tick is shown.', @@ -473,7 +473,7 @@ module.exports = { values: ['none', 'e', 'E', 'power', 'SI', 'B'], dflt: 'B', role: 'style', - editType: 'ticks+margins', + editType: 'ticks', description: [ 'Determines a formatting rule for the tick exponents.', 'For example, consider the number 1,000,000,000.', @@ -489,7 +489,7 @@ module.exports = { valType: 'boolean', dflt: false, role: 'style', - editType: 'ticks+margins', + editType: 'ticks', description: [ 'If "true", even 4-digit integers are separated' ].join(' ') @@ -498,7 +498,7 @@ module.exports = { valType: 'string', dflt: '', role: 'style', - editType: 'ticks+margins', + editType: 'ticks', description: [ 'Sets the tick label formatting rule using d3 formatting mini-languages', 'which are very similar to those in Python. For numbers, see:', @@ -517,10 +517,10 @@ module.exports = { valType: 'info_array', role: 'info', items: [ - {valType: 'any', editType: 'ticks+margins'}, - {valType: 'any', editType: 'ticks+margins'} + {valType: 'any', editType: 'ticks'}, + {valType: 'any', editType: 'ticks'} ], - editType: 'ticks+margins', + editType: 'ticks', description: [ 'range [*min*, *max*], where *min*, *max* - dtick values', 'which describe some zoom level, it is possible to omit *min*', @@ -531,12 +531,12 @@ module.exports = { valType: 'string', dflt: '', role: 'style', - editType: 'ticks+margins', + editType: 'ticks', description: [ 'string - dtickformat for described zoom level, the same as *tickformat*' ].join(' ') }, - editType: 'ticks+margins' + editType: 'ticks' }, hoverformat: { valType: 'string', @@ -638,7 +638,7 @@ module.exports = { constants.idRegex.y.toString() ], role: 'info', - editType: 'plot+margins', + editType: 'plot', description: [ 'If set to an opposite-letter axis id (e.g. `x2`, `y`), this axis is bound to', 'the corresponding opposite-letter axis.', @@ -651,7 +651,7 @@ module.exports = { valType: 'enumerated', values: ['top', 'bottom', 'left', 'right'], role: 'info', - editType: 'plot+margins', + editType: 'plot', description: [ 'Determines whether a x (y) axis is positioned', 'at the *bottom* (*left*) or *top* (*right*)', @@ -695,11 +695,11 @@ module.exports = { valType: 'info_array', role: 'info', items: [ - {valType: 'number', min: 0, max: 1, editType: 'plot+margins'}, - {valType: 'number', min: 0, max: 1, editType: 'plot+margins'} + {valType: 'number', min: 0, max: 1, editType: 'plot'}, + {valType: 'number', min: 0, max: 1, editType: 'plot'} ], dflt: [0, 1], - editType: 'plot+margins', + editType: 'plot', description: [ 'Sets the domain of this axis (in plot fraction).' ].join(' ') @@ -710,7 +710,7 @@ module.exports = { max: 1, dflt: 0, role: 'style', - editType: 'plot+margins', + editType: 'plot', description: [ 'Sets the position of this axis in the plotting space', '(in normalized coordinates).', @@ -754,7 +754,7 @@ module.exports = { autotick: { valType: 'boolean', role: 'info', - editType: 'ticks+margins', + editType: 'ticks', description: [ 'Obsolete.', 'Set `tickmode` to *auto* for old `autotick` *true* behavior.', diff --git a/src/plots/plots.js b/src/plots/plots.js index c49f5cb3ae7..d0e2f703b24 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -389,6 +389,9 @@ plots.supplyDefaults = function(gd, opts) { // eg set `_requestRangeslider.x2 = true` for xaxis2 newFullLayout._requestRangeslider = {}; + // pull uids from old data to use as new defaults + newFullLayout._traceUids = getTraceUids(oldFullData, newData); + // then do the data newFullLayout._globalTransforms = (gd._context || {}).globalTransforms; plots.supplyDataDefaults(newData, newFullData, newLayout, newFullLayout); @@ -499,6 +502,46 @@ plots.supplyDefaultsUpdateCalc = function(oldCalcdata, newFullData) { } }; +/** + * Create a list of uid strings satisfying (in this order of importance): + * 1. all unique, all strings + * 2. matches input uids if provided + * 3. matches previous data uids + */ +function getTraceUids(oldFullData, newData) { + var len = newData.length; + var oldFullInput = []; + var i, prevFullInput; + for(i = 0; i < oldFullData.length; i++) { + var thisFullInput = oldFullData[i]._fullInput; + if(thisFullInput !== prevFullInput) oldFullInput.push(thisFullInput); + prevFullInput = thisFullInput; + } + var oldLen = oldFullInput.length; + var out = new Array(len); + var seenUids = {}; + + function setUid(uid, i) { + out[i] = uid; + seenUids[uid] = 1; + } + + function tryUid(uid, i) { + if(uid && typeof uid === 'string' && !seenUids[uid]) { + setUid(uid, i); + return true; + } + } + + for(i = 0; i < len; i++) { + if(tryUid(newData[i].uid, i)) continue; + if(i < oldLen && tryUid(oldFullInput[i].uid, i)) continue; + setUid(Lib.randstr(seenUids), i); + } + + return out; +} + /** * Make a container for collecting subplots we need to display. * @@ -886,7 +929,11 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { for(i = 0; i < dataIn.length; i++) { trace = dataIn[i]; - fullTrace = plots.supplyTraceDefaults(trace, colorCnt, fullLayout, i); + fullTrace = plots.supplyTraceDefaults(trace, colorCnt, fullLayout, i, + // reuse uid we may have pulled out of oldFullData + fullLayout._traceUids[i]); + + fullTrace.uid = fullLayout._traceUids[i]; fullTrace.index = i; fullTrace._input = trace; @@ -897,16 +944,17 @@ 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); + var fullExpandedTrace = plots.supplyTraceDefaults( + expandedTrace, cnt, fullLayout, i, + // set uid using parent uid and expanded index + // to promote consistency between update calls + fullTrace.uid + j + ); // relink private (i.e. underscore) keys expanded trace to full expanded trace so // that transform supply-default methods can set _ keys for future use. relinkPrivateKeys(fullExpandedTrace, expandedTrace); - // mutate uid here using parent uid and expanded index - // to promote consistency between update calls - expandedTrace.uid = fullExpandedTrace.uid = fullTrace.uid + j; - // add info about parent data trace fullExpandedTrace.index = i; fullExpandedTrace._input = trace; @@ -1031,10 +1079,10 @@ plots.supplyFrameDefaults = function(frameIn) { return frameOut; }; -plots.supplyTraceDefaults = function(traceIn, colorIndex, layout, traceInIndex) { +plots.supplyTraceDefaults = function(traceIn, colorIndex, layout, traceInIndex, uid) { var colorway = layout.colorway || Color.defaults; - var traceOut = {}, - defaultColor = colorway[colorIndex % colorway.length]; + var traceOut = {uid: uid}; + var defaultColor = colorway[colorIndex % colorway.length]; var i; @@ -1045,7 +1093,6 @@ plots.supplyTraceDefaults = function(traceIn, colorIndex, layout, traceInIndex) var visible = coerce('visible'); coerce('type'); - coerce('uid'); coerce('name', layout._traceWord + ' ' + traceInIndex); // we want even invisible traces to make their would-be subplots visible @@ -1543,32 +1590,73 @@ plots.sanitizeMargins = function(fullLayout) { } }; -// called by components to see if we need to -// expand the margins to show them -// o is {x,l,r,y,t,b} where x and y are plot fractions, -// the rest are pixels in each direction -// or leave o out to delete this entry (like if it's hidden) +plots.clearAutoMarginIds = function(gd) { + gd._fullLayout._pushmarginIds = {}; +}; + +plots.allowAutoMargin = function(gd, id) { + gd._fullLayout._pushmarginIds[id] = 1; +}; + +function setupAutoMargin(fullLayout) { + if(!fullLayout._pushmargin) fullLayout._pushmargin = {}; + if(!fullLayout._pushmarginIds) fullLayout._pushmarginIds = {}; +} + +/** + * autoMargin: called by components that may need to expand the margins to + * be rendered on-plot. + * + * @param {DOM element} gd + * @param {string} id - an identifier unique (within this plot) to this object, + * so we can remove a previous margin expansion from the same object. + * @param {object} o - the margin requirements of this object, or omit to delete + * this entry (like if it's hidden). Keys are: + * x, y: plot fraction of the anchor point. + * xl, xr, yt, yb: if the object has an extent defined in plot fraction, + * you can specify both edges as plot fractions in each dimension + * l, r, t, b: the pixels to pad past the plot fraction x[l|r] and y[t|b] + * pad: extra pixels to add in all directions, default 12 (why?) + */ plots.autoMargin = function(gd, id, o) { var fullLayout = gd._fullLayout; - if(!fullLayout._pushmargin) fullLayout._pushmargin = {}; + setupAutoMargin(fullLayout); + + var pushMargin = fullLayout._pushmargin; + var pushMarginIds = fullLayout._pushmarginIds; if(fullLayout.margin.autoexpand !== false) { - if(!o) delete fullLayout._pushmargin[id]; + if(!o) { + delete pushMargin[id]; + delete pushMarginIds[id]; + } else { - var pad = o.pad === undefined ? 12 : o.pad; + var pad = o.pad; + if(pad === undefined) { + var margin = fullLayout.margin; + // if no explicit pad is given, use 12px unless there's a + // specified margin that's smaller than that + pad = Math.min(12, margin.l, margin.r, margin.t, margin.b); + } // if the item is too big, just give it enough automargin to // make sure you can still grab it and bring it back if(o.l + o.r > fullLayout.width * 0.5) o.l = o.r = 0; if(o.b + o.t > fullLayout.height * 0.5) o.b = o.t = 0; - fullLayout._pushmargin[id] = { - l: {val: o.x, size: o.l + pad}, - r: {val: o.x, size: o.r + pad}, - b: {val: o.y, size: o.b + pad}, - t: {val: o.y, size: o.t + pad} + var xl = o.xl !== undefined ? o.xl : o.x; + var xr = o.xr !== undefined ? o.xr : o.x; + var yt = o.yt !== undefined ? o.yt : o.y; + var yb = o.yb !== undefined ? o.yb : o.y; + + pushMargin[id] = { + l: {val: xl, size: o.l + pad}, + r: {val: xr, size: o.r + pad}, + b: {val: yb, size: o.b + pad}, + t: {val: yt, size: o.t + pad} }; + pushMarginIds[id] = 1; } if(!fullLayout._replotting) plots.doAutoMargin(gd); @@ -1578,7 +1666,7 @@ plots.autoMargin = function(gd, id, o) { plots.doAutoMargin = function(gd) { var fullLayout = gd._fullLayout; if(!fullLayout._size) fullLayout._size = {}; - if(!fullLayout._pushmargin) fullLayout._pushmargin = {}; + setupAutoMargin(fullLayout); var gs = fullLayout._size, oldmargins = JSON.stringify(gs); @@ -1586,16 +1674,21 @@ plots.doAutoMargin = function(gd) { // adjust margins for outside components // fullLayout.margin is the requested margin, // fullLayout._size has margins and plotsize after adjustment - var ml = Math.max(fullLayout.margin.l || 0, 0), - mr = Math.max(fullLayout.margin.r || 0, 0), - mt = Math.max(fullLayout.margin.t || 0, 0), - mb = Math.max(fullLayout.margin.b || 0, 0), - pm = fullLayout._pushmargin; + var ml = Math.max(fullLayout.margin.l || 0, 0); + var mr = Math.max(fullLayout.margin.r || 0, 0); + var mt = Math.max(fullLayout.margin.t || 0, 0); + var mb = Math.max(fullLayout.margin.b || 0, 0); + var pushMargin = fullLayout._pushmargin; + var pushMarginIds = fullLayout._pushmarginIds; if(fullLayout.margin.autoexpand !== false) { + for(var k in pushMargin) { + if(!pushMarginIds[k]) delete pushMargin[k]; + } + // fill in the requested margins - pm.base = { + pushMargin.base = { l: {val: 0, size: ml}, r: {val: 1, size: mr}, t: {val: 1, size: mt}, @@ -1605,19 +1698,19 @@ plots.doAutoMargin = function(gd) { // now cycle through all the combinations of l and r // (and t and b) to find the required margins - for(var k1 in pm) { + for(var k1 in pushMargin) { - var pushleft = pm[k1].l || {}, - pushbottom = pm[k1].b || {}, + var pushleft = pushMargin[k1].l || {}, + pushbottom = pushMargin[k1].b || {}, fl = pushleft.val, pl = pushleft.size, fb = pushbottom.val, pb = pushbottom.size; - for(var k2 in pm) { - if(isNumeric(pl) && pm[k2].r) { - var fr = pm[k2].r.val, - pr = pm[k2].r.size; + for(var k2 in pushMargin) { + if(isNumeric(pl) && pushMargin[k2].r) { + var fr = pushMargin[k2].r.val, + pr = pushMargin[k2].r.size; if(fr > fl) { var newl = (pl * fr + @@ -1631,9 +1724,9 @@ plots.doAutoMargin = function(gd) { } } - if(isNumeric(pb) && pm[k2].t) { - var ft = pm[k2].t.val, - pt = pm[k2].t.size; + if(isNumeric(pb) && pushMargin[k2].t) { + var ft = pushMargin[k2].t.val, + pt = pushMargin[k2].t.size; if(ft > fb) { var newb = (pb * ft + diff --git a/src/traces/bar/index.js b/src/traces/bar/index.js index de5c8f1cc2a..b05ddd8b79d 100644 --- a/src/traces/bar/index.js +++ b/src/traces/bar/index.js @@ -17,7 +17,7 @@ Bar.supplyDefaults = require('./defaults'); Bar.supplyLayoutDefaults = require('./layout_defaults'); Bar.calc = require('./calc'); Bar.setPositions = require('./set_positions'); -Bar.colorbar = require('../scatter/colorbar'); +Bar.colorbar = require('../scatter/marker_colorbar'); Bar.arraysToCalcdata = require('./arrays_to_calcdata'); Bar.plot = require('./plot'); Bar.style = require('./style').style; @@ -28,7 +28,7 @@ Bar.selectPoints = require('./select'); Bar.moduleType = 'trace'; Bar.name = 'bar'; Bar.basePlotModule = require('../../plots/cartesian'); -Bar.categories = ['cartesian', 'svg', 'bar', 'oriented', 'markerColorscale', 'errorBarsOK', 'showLegend', 'zoomScale']; +Bar.categories = ['cartesian', 'svg', 'bar', 'oriented', 'errorBarsOK', 'showLegend', 'zoomScale']; Bar.meta = { description: [ 'The data visualized by the span of the bars is set in `y`', diff --git a/src/traces/cone/colorbar.js b/src/traces/cone/colorbar.js deleted file mode 100644 index 412de6bfced..00000000000 --- a/src/traces/cone/colorbar.js +++ /dev/null @@ -1,37 +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 Plots = require('../../plots/plots'); -var Colorscale = require('../../components/colorscale'); -var drawColorbar = require('../../components/colorbar/draw'); - -module.exports = function colorbar(gd, cd) { - var trace = cd[0].trace; - var cbId = 'cb' + trace.uid; - var cmin = trace.cmin; - var cmax = trace.cmax; - - gd._fullLayout._infolayer.selectAll('.' + cbId).remove(); - - if(!trace.showscale) { - Plots.autoMargin(gd, cbId); - return; - } - - var cb = cd[0].t.cb = drawColorbar(gd, cbId); - var sclFunc = Colorscale.makeColorScaleFunc( - Colorscale.extractScale(trace.colorscale, cmin, cmax), - {noNumericCheck: true} - ); - - cb.fillcolor(sclFunc) - .filllevels({start: cmin, end: cmax, size: (cmax - cmin) / 254}) - .options(trace.colorbar)(); -}; diff --git a/src/traces/cone/index.js b/src/traces/cone/index.js index 2af8d31f5fd..fb15f5ac6e5 100644 --- a/src/traces/cone/index.js +++ b/src/traces/cone/index.js @@ -16,7 +16,10 @@ module.exports = { attributes: require('./attributes'), supplyDefaults: require('./defaults'), - colorbar: require('./colorbar'), + colorbar: { + min: 'cmin', + max: 'cmax' + }, calc: require('./calc'), plot: require('./convert'), diff --git a/src/traces/contour/colorbar.js b/src/traces/contour/colorbar.js index 82f87853f58..9b800339d5d 100644 --- a/src/traces/contour/colorbar.js +++ b/src/traces/contour/colorbar.js @@ -9,7 +9,6 @@ 'use strict'; -var Plots = require('../../plots/plots'); var drawColorbar = require('../../components/colorbar/draw'); var makeColorMap = require('./make_color_map'); @@ -22,10 +21,7 @@ module.exports = function colorbar(gd, cd) { gd._fullLayout._infolayer.selectAll('.' + cbId).remove(); - if(!trace.showscale) { - Plots.autoMargin(gd, cbId); - return; - } + if(!trace.showscale) return; var cb = drawColorbar(gd, cbId); cd[0].t.cb = cb; diff --git a/src/traces/heatmap/colorbar.js b/src/traces/heatmap/colorbar.js index d76f44087c5..5093ed98703 100644 --- a/src/traces/heatmap/colorbar.js +++ b/src/traces/heatmap/colorbar.js @@ -6,44 +6,9 @@ * LICENSE file in the root directory of this source tree. */ - 'use strict'; -var isNumeric = require('fast-isnumeric'); - -var Lib = require('../../lib'); -var Plots = require('../../plots/plots'); -var Colorscale = require('../../components/colorscale'); -var drawColorbar = require('../../components/colorbar/draw'); - - -module.exports = function colorbar(gd, cd) { - var trace = cd[0].trace, - cbId = 'cb' + trace.uid, - zmin = trace.zmin, - zmax = trace.zmax; - - if(!isNumeric(zmin)) zmin = Lib.aggNums(Math.min, null, trace.z); - if(!isNumeric(zmax)) zmax = Lib.aggNums(Math.max, null, trace.z); - - gd._fullLayout._infolayer.selectAll('.' + cbId).remove(); - - if(!trace.showscale) { - Plots.autoMargin(gd, cbId); - return; - } - - var cb = cd[0].t.cb = drawColorbar(gd, cbId); - var sclFunc = Colorscale.makeColorScaleFunc( - Colorscale.extractScale( - trace.colorscale, - zmin, - zmax - ), - { noNumericCheck: true } - ); - - cb.fillcolor(sclFunc) - .filllevels({start: zmin, end: zmax, size: (zmax - zmin) / 254}) - .options(trace.colorbar)(); +module.exports = { + min: 'zmin', + max: 'zmax' }; diff --git a/src/traces/histogram/index.js b/src/traces/histogram/index.js index 8ee3041badb..c20e31d6e55 100644 --- a/src/traces/histogram/index.js +++ b/src/traces/histogram/index.js @@ -35,7 +35,7 @@ Histogram.plot = require('../bar/plot'); Histogram.layerName = 'barlayer'; Histogram.style = require('../bar/style').style; Histogram.styleOnSelect = require('../bar/style').styleOnSelect; -Histogram.colorbar = require('../scatter/colorbar'); +Histogram.colorbar = require('../scatter/marker_colorbar'); Histogram.hoverPoints = require('./hover'); Histogram.selectPoints = require('../bar/select'); Histogram.eventData = require('./event_data'); diff --git a/src/traces/mesh3d/colorbar.js b/src/traces/mesh3d/colorbar.js deleted file mode 100644 index 52e06998852..00000000000 --- a/src/traces/mesh3d/colorbar.js +++ /dev/null @@ -1,48 +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 isNumeric = require('fast-isnumeric'); - -var Lib = require('../../lib'); -var Plots = require('../../plots/plots'); -var Colorscale = require('../../components/colorscale'); -var drawColorbar = require('../../components/colorbar/draw'); - -module.exports = function colorbar(gd, cd) { - var trace = cd[0].trace, - cbId = 'cb' + trace.uid, - cmin = trace.cmin, - cmax = trace.cmax, - vals = trace.intensity || []; - - if(!isNumeric(cmin)) cmin = Lib.aggNums(Math.min, null, vals); - if(!isNumeric(cmax)) cmax = Lib.aggNums(Math.max, null, vals); - - gd._fullLayout._infolayer.selectAll('.' + cbId).remove(); - - if(!trace.showscale) { - Plots.autoMargin(gd, cbId); - return; - } - - var cb = cd[0].t.cb = drawColorbar(gd, cbId); - var sclFunc = Colorscale.makeColorScaleFunc( - Colorscale.extractScale( - trace.colorscale, - cmin, - cmax - ), - { noNumericCheck: true } - ); - - cb.fillcolor(sclFunc) - .filllevels({start: cmin, end: cmax, size: (cmax - cmin) / 254}) - .options(trace.colorbar)(); -}; diff --git a/src/traces/mesh3d/index.js b/src/traces/mesh3d/index.js index 11f573866ff..0058bdb89ee 100644 --- a/src/traces/mesh3d/index.js +++ b/src/traces/mesh3d/index.js @@ -14,7 +14,10 @@ var Mesh3D = {}; Mesh3D.attributes = require('./attributes'); Mesh3D.supplyDefaults = require('./defaults'); Mesh3D.calc = require('./calc'); -Mesh3D.colorbar = require('./colorbar'); +Mesh3D.colorbar = { + min: 'cmin', + max: 'cmax' +}; Mesh3D.plot = require('./convert'); Mesh3D.moduleType = 'trace'; diff --git a/src/traces/parcoords/colorbar.js b/src/traces/parcoords/colorbar.js deleted file mode 100644 index 2f4cdd26e34..00000000000 --- a/src/traces/parcoords/colorbar.js +++ /dev/null @@ -1,52 +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 isNumeric = require('fast-isnumeric'); - -var Lib = require('../../lib'); -var Plots = require('../../plots/plots'); -var Colorscale = require('../../components/colorscale'); -var drawColorbar = require('../../components/colorbar/draw'); - - -module.exports = function colorbar(gd, cd) { - var trace = cd[0].trace, - line = trace.line, - cbId = 'cb' + trace.uid; - - gd._fullLayout._infolayer.selectAll('.' + cbId).remove(); - - if((line === undefined) || !line.showscale) { - Plots.autoMargin(gd, cbId); - return; - } - - var vals = line.color, - cmin = line.cmin, - cmax = line.cmax; - - if(!isNumeric(cmin)) cmin = Lib.aggNums(Math.min, null, vals); - if(!isNumeric(cmax)) cmax = Lib.aggNums(Math.max, null, vals); - - var cb = cd[0].t.cb = drawColorbar(gd, cbId); - var sclFunc = Colorscale.makeColorScaleFunc( - Colorscale.extractScale( - line.colorscale, - cmin, - cmax - ), - { noNumericCheck: true } - ); - - cb.fillcolor(sclFunc) - .filllevels({start: cmin, end: cmax, size: (cmax - cmin) / 254}) - .options(line.colorbar)(); -}; diff --git a/src/traces/parcoords/index.js b/src/traces/parcoords/index.js index bd514968329..35a2931d978 100644 --- a/src/traces/parcoords/index.js +++ b/src/traces/parcoords/index.js @@ -14,7 +14,11 @@ Parcoords.attributes = require('./attributes'); Parcoords.supplyDefaults = require('./defaults'); Parcoords.calc = require('./calc'); Parcoords.plot = require('./plot'); -Parcoords.colorbar = require('./colorbar'); +Parcoords.colorbar = { + container: 'line', + min: 'cmin', + max: 'cmax' +}; Parcoords.moduleType = 'trace'; Parcoords.name = 'parcoords'; diff --git a/src/traces/scatter/colorbar.js b/src/traces/scatter/colorbar.js deleted file mode 100644 index e5e58f27a42..00000000000 --- a/src/traces/scatter/colorbar.js +++ /dev/null @@ -1,54 +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 isNumeric = require('fast-isnumeric'); - -var Lib = require('../../lib'); -var Plots = require('../../plots/plots'); -var Colorscale = require('../../components/colorscale'); -var drawColorbar = require('../../components/colorbar/draw'); - - -module.exports = function colorbar(gd, cd) { - var trace = cd[0].trace, - marker = trace.marker, - cbId = 'cb' + trace.uid; - - gd._fullLayout._infolayer.selectAll('.' + cbId).remove(); - - // TODO make Colorbar.draw support multiple colorbar per trace - - if((marker === undefined) || !marker.showscale) { - Plots.autoMargin(gd, cbId); - return; - } - - var vals = marker.color, - cmin = marker.cmin, - cmax = marker.cmax; - - if(!isNumeric(cmin)) cmin = Lib.aggNums(Math.min, null, vals); - if(!isNumeric(cmax)) cmax = Lib.aggNums(Math.max, null, vals); - - var cb = cd[0].t.cb = drawColorbar(gd, cbId); - var sclFunc = Colorscale.makeColorScaleFunc( - Colorscale.extractScale( - marker.colorscale, - cmin, - cmax - ), - { noNumericCheck: true } - ); - - cb.fillcolor(sclFunc) - .filllevels({start: cmin, end: cmax, size: (cmax - cmin) / 254}) - .options(marker.colorbar)(); -}; diff --git a/src/traces/scatter/index.js b/src/traces/scatter/index.js index 1641f5d608a..020bc06a511 100644 --- a/src/traces/scatter/index.js +++ b/src/traces/scatter/index.js @@ -17,15 +17,13 @@ Scatter.hasMarkers = subtypes.hasMarkers; Scatter.hasText = subtypes.hasText; Scatter.isBubble = subtypes.isBubble; -// traces with < this many points are by default shown -// with points and lines, > just get lines Scatter.attributes = require('./attributes'); Scatter.supplyDefaults = require('./defaults'); Scatter.cleanData = require('./clean_data'); Scatter.calc = require('./calc').calc; Scatter.arraysToCalcdata = require('./arrays_to_calcdata'); Scatter.plot = require('./plot'); -Scatter.colorbar = require('./colorbar'); +Scatter.colorbar = require('./marker_colorbar'); Scatter.style = require('./style').style; Scatter.styleOnSelect = require('./style').styleOnSelect; Scatter.hoverPoints = require('./hover'); @@ -35,7 +33,7 @@ Scatter.animatable = true; Scatter.moduleType = 'trace'; Scatter.name = 'scatter'; Scatter.basePlotModule = require('../../plots/cartesian'); -Scatter.categories = ['cartesian', 'svg', 'symbols', 'markerColorscale', 'errorBarsOK', 'showLegend', 'scatter-like', 'zoomScale']; +Scatter.categories = ['cartesian', 'svg', 'symbols', 'errorBarsOK', 'showLegend', 'scatter-like', 'zoomScale']; Scatter.meta = { description: [ 'The scatter trace type encompasses line charts, scatter charts, text charts, and bubble charts.', diff --git a/src/traces/scatter/marker_colorbar.js b/src/traces/scatter/marker_colorbar.js new file mode 100644 index 00000000000..111bb2d75d9 --- /dev/null +++ b/src/traces/scatter/marker_colorbar.js @@ -0,0 +1,16 @@ +/** +* 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 = { + container: 'marker', + min: 'cmin', + max: 'cmax' +}; diff --git a/src/traces/scatter3d/index.js b/src/traces/scatter3d/index.js index 495f110818f..fea4e6adec2 100644 --- a/src/traces/scatter3d/index.js +++ b/src/traces/scatter3d/index.js @@ -14,13 +14,13 @@ Scatter3D.plot = require('./convert'); Scatter3D.attributes = require('./attributes'); Scatter3D.markerSymbols = require('../../constants/gl3d_markers'); Scatter3D.supplyDefaults = require('./defaults'); -Scatter3D.colorbar = require('../scatter/colorbar'); +Scatter3D.colorbar = require('../scatter/marker_colorbar'); Scatter3D.calc = require('./calc'); Scatter3D.moduleType = 'trace'; Scatter3D.name = 'scatter3d'; Scatter3D.basePlotModule = require('../../plots/gl3d'); -Scatter3D.categories = ['gl3d', 'symbols', 'markerColorscale', 'showLegend']; +Scatter3D.categories = ['gl3d', 'symbols', 'showLegend']; Scatter3D.meta = { hrName: 'scatter_3d', description: [ diff --git a/src/traces/scattercarpet/index.js b/src/traces/scattercarpet/index.js index fa7952da419..eba9c0f0b66 100644 --- a/src/traces/scattercarpet/index.js +++ b/src/traces/scattercarpet/index.js @@ -12,7 +12,7 @@ var ScatterCarpet = {}; ScatterCarpet.attributes = require('./attributes'); ScatterCarpet.supplyDefaults = require('./defaults'); -ScatterCarpet.colorbar = require('../scatter/colorbar'); +ScatterCarpet.colorbar = require('../scatter/marker_colorbar'); ScatterCarpet.calc = require('./calc'); ScatterCarpet.plot = require('./plot'); ScatterCarpet.style = require('../scatter/style').style; @@ -24,7 +24,7 @@ ScatterCarpet.eventData = require('./event_data'); ScatterCarpet.moduleType = 'trace'; ScatterCarpet.name = 'scattercarpet'; ScatterCarpet.basePlotModule = require('../../plots/cartesian'); -ScatterCarpet.categories = ['svg', 'carpet', 'symbols', 'markerColorscale', 'showLegend', 'carpetDependent', 'zoomScale']; +ScatterCarpet.categories = ['svg', 'carpet', 'symbols', 'showLegend', 'carpetDependent', 'zoomScale']; ScatterCarpet.meta = { hrName: 'scatter_carpet', description: [ diff --git a/src/traces/scattergeo/index.js b/src/traces/scattergeo/index.js index a176bc39c30..1f2976735c5 100644 --- a/src/traces/scattergeo/index.js +++ b/src/traces/scattergeo/index.js @@ -13,7 +13,7 @@ var ScatterGeo = {}; ScatterGeo.attributes = require('./attributes'); ScatterGeo.supplyDefaults = require('./defaults'); -ScatterGeo.colorbar = require('../scatter/colorbar'); +ScatterGeo.colorbar = require('../scatter/marker_colorbar'); ScatterGeo.calc = require('./calc'); ScatterGeo.plot = require('./plot'); ScatterGeo.style = require('./style'); @@ -25,7 +25,7 @@ ScatterGeo.selectPoints = require('./select'); ScatterGeo.moduleType = 'trace'; ScatterGeo.name = 'scattergeo'; ScatterGeo.basePlotModule = require('../../plots/geo'); -ScatterGeo.categories = ['geo', 'symbols', 'markerColorscale', 'showLegend', 'scatter-like']; +ScatterGeo.categories = ['geo', 'symbols', 'showLegend', 'scatter-like']; ScatterGeo.meta = { hrName: 'scatter_geo', description: [ diff --git a/src/traces/scattergl/index.js b/src/traces/scattergl/index.js index 17b2096e37c..6a5181e5108 100644 --- a/src/traces/scattergl/index.js +++ b/src/traces/scattergl/index.js @@ -824,12 +824,12 @@ module.exports = { moduleType: 'trace', name: 'scattergl', basePlotModule: require('../../plots/cartesian'), - categories: ['gl', 'regl', 'cartesian', 'symbols', 'errorBarsOK', 'markerColorscale', 'showLegend', 'scatter-like'], + categories: ['gl', 'regl', 'cartesian', 'symbols', 'errorBarsOK', 'showLegend', 'scatter-like'], attributes: require('./attributes'), supplyDefaults: require('./defaults'), cleanData: require('../scatter/clean_data'), - colorbar: require('../scatter/colorbar'), + colorbar: require('../scatter/marker_colorbar'), calc: calc, plot: plot, hoverPoints: hoverPoints, diff --git a/src/traces/scattermapbox/index.js b/src/traces/scattermapbox/index.js index 79405e4adfc..ea58c0936b8 100644 --- a/src/traces/scattermapbox/index.js +++ b/src/traces/scattermapbox/index.js @@ -13,7 +13,7 @@ var ScatterMapbox = {}; ScatterMapbox.attributes = require('./attributes'); ScatterMapbox.supplyDefaults = require('./defaults'); -ScatterMapbox.colorbar = require('../scatter/colorbar'); +ScatterMapbox.colorbar = require('../scatter/marker_colorbar'); ScatterMapbox.calc = require('../scattergeo/calc'); ScatterMapbox.plot = require('./plot'); ScatterMapbox.hoverPoints = require('./hover'); @@ -30,7 +30,7 @@ ScatterMapbox.style = function(_, cd) { ScatterMapbox.moduleType = 'trace'; ScatterMapbox.name = 'scattermapbox'; ScatterMapbox.basePlotModule = require('../../plots/mapbox'); -ScatterMapbox.categories = ['mapbox', 'gl', 'symbols', 'markerColorscale', 'showLegend', 'scatterlike']; +ScatterMapbox.categories = ['mapbox', 'gl', 'symbols', 'showLegend', 'scatterlike']; ScatterMapbox.meta = { hrName: 'scatter_mapbox', description: [ diff --git a/src/traces/scatterpolar/index.js b/src/traces/scatterpolar/index.js index 4b16c1c7fed..ece30328990 100644 --- a/src/traces/scatterpolar/index.js +++ b/src/traces/scatterpolar/index.js @@ -12,10 +12,11 @@ module.exports = { moduleType: 'trace', name: 'scatterpolar', basePlotModule: require('../../plots/polar'), - categories: ['polar', 'symbols', 'markerColorscale', 'showLegend', 'scatter-like'], + categories: ['polar', 'symbols', 'showLegend', 'scatter-like'], attributes: require('./attributes'), supplyDefaults: require('./defaults'), + colorbar: require('../scatter/marker_colorbar'), calc: require('./calc'), plot: require('./plot'), style: require('../scatter/style').style, diff --git a/src/traces/scatterpolargl/index.js b/src/traces/scatterpolargl/index.js index b5aa5c17e5f..058767fd875 100644 --- a/src/traces/scatterpolargl/index.js +++ b/src/traces/scatterpolargl/index.js @@ -184,10 +184,11 @@ module.exports = { moduleType: 'trace', name: 'scatterpolargl', basePlotModule: require('../../plots/polar'), - categories: ['gl', 'regl', 'polar', 'symbols', 'markerColorscale', 'showLegend', 'scatter-like'], + categories: ['gl', 'regl', 'polar', 'symbols', 'showLegend', 'scatter-like'], attributes: require('./attributes'), supplyDefaults: require('./defaults'), + colorbar: require('../scatter/marker_colorbar'), calc: calc, plot: plot, diff --git a/src/traces/scatterternary/index.js b/src/traces/scatterternary/index.js index 2b3f1fbe5bc..ba7b7e15c36 100644 --- a/src/traces/scatterternary/index.js +++ b/src/traces/scatterternary/index.js @@ -12,7 +12,7 @@ var ScatterTernary = {}; ScatterTernary.attributes = require('./attributes'); ScatterTernary.supplyDefaults = require('./defaults'); -ScatterTernary.colorbar = require('../scatter/colorbar'); +ScatterTernary.colorbar = require('../scatter/marker_colorbar'); ScatterTernary.calc = require('./calc'); ScatterTernary.plot = require('./plot'); ScatterTernary.style = require('../scatter/style').style; @@ -24,7 +24,7 @@ ScatterTernary.eventData = require('./event_data'); ScatterTernary.moduleType = 'trace'; ScatterTernary.name = 'scatterternary'; ScatterTernary.basePlotModule = require('../../plots/ternary'); -ScatterTernary.categories = ['ternary', 'symbols', 'markerColorscale', 'showLegend', 'scatter-like']; +ScatterTernary.categories = ['ternary', 'symbols', 'showLegend', 'scatter-like']; ScatterTernary.meta = { hrName: 'scatter_ternary', description: [ diff --git a/src/traces/splom/index.js b/src/traces/splom/index.js index c7f7f80b6a9..efcd775b117 100644 --- a/src/traces/splom/index.js +++ b/src/traces/splom/index.js @@ -460,10 +460,11 @@ module.exports = { name: 'splom', basePlotModule: require('./base_plot'), - categories: ['gl', 'regl', 'cartesian', 'symbols', 'markerColorscale', 'showLegend', 'scatter-like'], + categories: ['gl', 'regl', 'cartesian', 'symbols', 'showLegend', 'scatter-like'], attributes: require('./attributes'), supplyDefaults: require('./defaults'), + colorbar: require('../scatter/marker_colorbar'), calc: calc, plot: plot, diff --git a/src/traces/surface/colorbar.js b/src/traces/surface/colorbar.js deleted file mode 100644 index 7d23c33df96..00000000000 --- a/src/traces/surface/colorbar.js +++ /dev/null @@ -1,50 +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 isNumeric = require('fast-isnumeric'); - -var Lib = require('../../lib'); -var Plots = require('../../plots/plots'); -var Colorscale = require('../../components/colorscale'); -var drawColorbar = require('../../components/colorbar/draw'); - - -module.exports = function colorbar(gd, cd) { - var trace = cd[0].trace, - cbId = 'cb' + trace.uid, - cmin = trace.cmin, - cmax = trace.cmax, - vals = trace.surfacecolor || trace.z; - - if(!isNumeric(cmin)) cmin = Lib.aggNums(Math.min, null, vals); - if(!isNumeric(cmax)) cmax = Lib.aggNums(Math.max, null, vals); - - gd._fullLayout._infolayer.selectAll('.' + cbId).remove(); - - if(!trace.showscale) { - Plots.autoMargin(gd, cbId); - return; - } - - var cb = cd[0].t.cb = drawColorbar(gd, cbId); - var sclFunc = Colorscale.makeColorScaleFunc( - Colorscale.extractScale( - trace.colorscale, - cmin, - cmax - ), - { noNumericCheck: true } - ); - - cb.fillcolor(sclFunc) - .filllevels({start: cmin, end: cmax, size: (cmax - cmin) / 254}) - .options(trace.colorbar)(); -}; diff --git a/src/traces/surface/index.js b/src/traces/surface/index.js index 480b2617558..fd8dec8e1ac 100644 --- a/src/traces/surface/index.js +++ b/src/traces/surface/index.js @@ -13,7 +13,10 @@ var Surface = {}; Surface.attributes = require('./attributes'); Surface.supplyDefaults = require('./defaults'); -Surface.colorbar = require('./colorbar'); +Surface.colorbar = { + min: 'cmin', + max: 'cmax' +}; Surface.calc = require('./calc'); Surface.plot = require('./convert'); diff --git a/test/image/baselines/11.png b/test/image/baselines/11.png index 0b5e7486d53..52ca085761f 100644 Binary files a/test/image/baselines/11.png and b/test/image/baselines/11.png differ diff --git a/test/image/baselines/15.png b/test/image/baselines/15.png index cb368944185..ef3f11f5404 100644 Binary files a/test/image/baselines/15.png and b/test/image/baselines/15.png differ diff --git a/test/image/baselines/28.png b/test/image/baselines/28.png index 5c6e382b07e..778a85873ed 100644 Binary files a/test/image/baselines/28.png and b/test/image/baselines/28.png differ diff --git a/test/image/baselines/5.png b/test/image/baselines/5.png index 6d0396fb097..9c2719bf9bb 100644 Binary files a/test/image/baselines/5.png and b/test/image/baselines/5.png differ diff --git a/test/image/baselines/benchmarks.png b/test/image/baselines/benchmarks.png index c2186f5ed1f..f5e1747aecd 100644 Binary files a/test/image/baselines/benchmarks.png and b/test/image/baselines/benchmarks.png differ diff --git a/test/image/baselines/dendrogram.png b/test/image/baselines/dendrogram.png index e94e7bf572d..5de973ec15f 100644 Binary files a/test/image/baselines/dendrogram.png and b/test/image/baselines/dendrogram.png differ diff --git a/test/image/baselines/gl3d_cufflinks.png b/test/image/baselines/gl3d_cufflinks.png index 9568022f2a5..e5fa56bd3ef 100644 Binary files a/test/image/baselines/gl3d_cufflinks.png and b/test/image/baselines/gl3d_cufflinks.png differ diff --git a/test/image/baselines/gl3d_ibm-plot.png b/test/image/baselines/gl3d_ibm-plot.png index 7ee9d3a6045..e9fee420a36 100644 Binary files a/test/image/baselines/gl3d_ibm-plot.png and b/test/image/baselines/gl3d_ibm-plot.png differ diff --git a/test/image/baselines/gl3d_opacity-scaling-spikes.png b/test/image/baselines/gl3d_opacity-scaling-spikes.png index 433d49e74e1..4c10912c143 100644 Binary files a/test/image/baselines/gl3d_opacity-scaling-spikes.png and b/test/image/baselines/gl3d_opacity-scaling-spikes.png differ diff --git a/test/image/baselines/glpolar_style.png b/test/image/baselines/glpolar_style.png index ed27de1ca04..31c2b18b375 100644 Binary files a/test/image/baselines/glpolar_style.png and b/test/image/baselines/glpolar_style.png differ diff --git a/test/image/baselines/polar_subplots.png b/test/image/baselines/polar_subplots.png index ad3d762b395..140e0aac811 100644 Binary files a/test/image/baselines/polar_subplots.png and b/test/image/baselines/polar_subplots.png differ diff --git a/test/image/baselines/splom_log.png b/test/image/baselines/splom_log.png index c5a1b8b3bb4..58e5ef0a28d 100644 Binary files a/test/image/baselines/splom_log.png and b/test/image/baselines/splom_log.png differ diff --git a/test/image/mocks/glpolar_style.json b/test/image/mocks/glpolar_style.json index eb111f1a7de..7e1d12d6681 100644 --- a/test/image/mocks/glpolar_style.json +++ b/test/image/mocks/glpolar_style.json @@ -18,6 +18,11 @@ "type": "scatterpolargl", "r": [50, 300, 900], "theta": [0, 90, 180], + "marker": { + "size": 20, + "color": [1, 2, 3], + "showscale": true + }, "subplot": "polar3" }, { "type": "scatterpolargl", diff --git a/test/image/mocks/polar_subplots.json b/test/image/mocks/polar_subplots.json index 28df993bda8..a7c69519f9b 100644 --- a/test/image/mocks/polar_subplots.json +++ b/test/image/mocks/polar_subplots.json @@ -21,7 +21,12 @@ "text": ["A1", "B1", "C1"], "hovertext": ["hover A1", "hover B1", "hover C1"], "line": {"shape": "spline"}, - "marker": {"symbol": "square", "size": 15}, + "marker": { + "symbol": "square", + "size": 15, + "color": [-1, 1, 2], + "showscale": true + }, "textfont": { "color": "green", "size": [20, 15, 10] diff --git a/test/image/mocks/splom_log.json b/test/image/mocks/splom_log.json index a7adf6f99a0..8ec446f0c26 100644 --- a/test/image/mocks/splom_log.json +++ b/test/image/mocks/splom_log.json @@ -12,7 +12,8 @@ "size": 20, "line": {"width": 2, "color": "#444"}, "color": [10, 40, 100], - "colorscale": "Reds" + "colorscale": "Reds", + "showscale": true } }], "layout": { diff --git a/test/jasmine/assets/custom_assertions.js b/test/jasmine/assets/custom_assertions.js index ba791a38250..2f4ca27302e 100644 --- a/test/jasmine/assets/custom_assertions.js +++ b/test/jasmine/assets/custom_assertions.js @@ -216,3 +216,34 @@ exports.assertElemInside = function(elem, container, msg) { contBB.top < elemBB.top && contBB.bottom > elemBB.bottom).toBe(true, msg); }; + +/* + * quick plot area dimension check: test width and/or height of the inner + * plot area (single subplot) to verify that the margins are as expected + * + * Note: if you use margin.pad on the plot, width and height will be larger + * than you expected by twice that padding. + * + * opts can have keys (all optional): + * width (exact width match) + * height (exact height match) + * widthLessThan (width must be less than this) + * heightLessThan (height must be less than this) + */ +exports.assertPlotSize = function(opts, msg) { + var width = opts.width; + var height = opts.height; + var widthLessThan = opts.widthLessThan; + var heightLessThan = opts.heightLessThan; + + var plotBB = d3.select('.bglayer .bg').node().getBoundingClientRect(); + var actualWidth = plotBB.width; + var actualHeight = plotBB.height; + + var msgPlus = msg ? ': ' + msg : ''; + + if(width) expect(actualWidth).toBeWithin(width, 1, 'width' + msgPlus); + if(height) expect(actualHeight).toBeWithin(height, 1, 'height' + msgPlus); + if(widthLessThan) expect(actualWidth).toBeLessThan(widthLessThan - 1, 'widthLessThan' + msgPlus); + if(heightLessThan) expect(actualHeight).toBeLessThan(heightLessThan - 1, 'heightLessThan' + msgPlus); +}; diff --git a/test/jasmine/assets/delay.js b/test/jasmine/assets/delay.js index 8e57a7bf840..5e7d8cbf26f 100644 --- a/test/jasmine/assets/delay.js +++ b/test/jasmine/assets/delay.js @@ -5,6 +5,10 @@ * like the `delay` module. * * Promise.resolve().then(delay(50)).then(...); + * + * or: + * + * delay(50)().then(...); */ module.exports = function delay(duration) { return function(value) { diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 22c99aac4bc..5f306a750b9 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -2631,11 +2631,9 @@ describe('Test axes', function() { Plotly.plot(gd, data) .then(function() { - initialSize = Lib.extendDeep({}, gd._fullLayout._size); expect(gd._fullLayout.xaxis._lastangle).toBe(30); - }) - .then(function() { - previousSize = Lib.extendDeep({}, gd._fullLayout._size); + + initialSize = previousSize = Lib.extendDeep({}, gd._fullLayout._size); return Plotly.relayout(gd, {'yaxis.automargin': true}); }) .then(function() { @@ -2644,9 +2642,8 @@ describe('Test axes', function() { expect(size.r).toBe(previousSize.r); expect(size.b).toBe(previousSize.b); expect(size.t).toBe(previousSize.t); - }) - .then(function() { - previousSize = Lib.extendDeep({}, gd._fullLayout._size); + + previousSize = Lib.extendDeep({}, size); return Plotly.relayout(gd, {'xaxis.automargin': true}); }) .then(function() { @@ -2655,21 +2652,53 @@ describe('Test axes', function() { expect(size.r).toBe(previousSize.r); expect(size.b).toBeGreaterThan(previousSize.b); expect(size.t).toBe(previousSize.t); + + previousSize = Lib.extendDeep({}, size); + savedBottom = previousSize.b; + + // move all the long x labels off-screen + return Plotly.relayout(gd, {'xaxis.range': [-10, -5]}); }) .then(function() { - previousSize = Lib.extendDeep({}, gd._fullLayout._size); - savedBottom = previousSize.b; - return Plotly.relayout(gd, {'xaxis.tickangle': 45}); + var size = gd._fullLayout._size; + expect(size.l).toBe(previousSize.l); + expect(size.r).toBe(previousSize.r); + expect(size.t).toBe(previousSize.t); + expect(size.b).toBe(initialSize.b); + + // move all the long y labels off-screen + return Plotly.relayout(gd, {'yaxis.range': [-10, -5]}); + }) + .then(function() { + var size = gd._fullLayout._size; + expect(size.l).toBe(initialSize.l); + expect(size.r).toBe(previousSize.r); + expect(size.t).toBe(previousSize.t); + expect(size.b).toBe(initialSize.b); + + // bring the long labels back + return Plotly.relayout(gd, { + 'xaxis.autorange': true, + 'yaxis.autorange': true + }); }) .then(function() { var size = gd._fullLayout._size; expect(size.l).toBe(previousSize.l); expect(size.r).toBe(previousSize.r); - expect(size.b).toBeGreaterThan(previousSize.b); expect(size.t).toBe(previousSize.t); + expect(size.b).toBe(previousSize.b); + + return Plotly.relayout(gd, {'xaxis.tickangle': 45}); }) .then(function() { - previousSize = Lib.extendDeep({}, gd._fullLayout._size); + var size = gd._fullLayout._size; + expect(size.l).toBe(previousSize.l); + expect(size.r).toBe(previousSize.r); + expect(size.b).toBeGreaterThan(previousSize.b); + expect(size.t).toBe(previousSize.t); + + previousSize = Lib.extendDeep({}, size); return Plotly.relayout(gd, {'xaxis.tickangle': 30}); }) .then(function() { @@ -2678,9 +2707,8 @@ describe('Test axes', function() { expect(size.r).toBe(previousSize.r); expect(size.b).toBe(savedBottom); expect(size.t).toBe(previousSize.t); - }) - .then(function() { - previousSize = Lib.extendDeep({}, gd._fullLayout._size); + + previousSize = Lib.extendDeep({}, size); return Plotly.relayout(gd, {'yaxis.ticklen': 30}); }) .then(function() { @@ -2689,17 +2717,15 @@ describe('Test axes', function() { expect(size.r).toBe(previousSize.r); expect(size.b).toBe(previousSize.b); expect(size.t).toBe(previousSize.t); - }) - .then(function() { - previousSize = Lib.extendDeep({}, gd._fullLayout._size); + + previousSize = Lib.extendDeep({}, size); return Plotly.relayout(gd, {'yaxis.titlefont.size': 30}); }) .then(function() { var size = gd._fullLayout._size; expect(size).toEqual(previousSize); - }) - .then(function() { - previousSize = Lib.extendDeep({}, gd._fullLayout._size); + + previousSize = Lib.extendDeep({}, size); return Plotly.relayout(gd, {'yaxis.title': 'hello'}); }) .then(function() { @@ -2708,10 +2734,9 @@ describe('Test axes', function() { expect(size.r).toBe(previousSize.r); expect(size.b).toBe(previousSize.b); expect(size.t).toBe(previousSize.t); - }) - .then(function() { - previousSize = Lib.extendDeep({}, gd._fullLayout._size); - return Plotly.relayout(gd, { 'yaxis.anchor': 'free' }); + + previousSize = Lib.extendDeep({}, size); + return Plotly.relayout(gd, {'yaxis.anchor': 'free'}); }) .then(function() { var size = gd._fullLayout._size; @@ -2719,10 +2744,9 @@ describe('Test axes', function() { expect(size.r).toBe(previousSize.r); expect(size.b).toBe(previousSize.b); expect(size.t).toBe(previousSize.t); - }) - .then(function() { - previousSize = Lib.extendDeep({}, gd._fullLayout._size); - return Plotly.relayout(gd, { 'yaxis.position': 0.1}); + + previousSize = Lib.extendDeep({}, size); + return Plotly.relayout(gd, {'yaxis.position': 0.1}); }) .then(function() { var size = gd._fullLayout._size; @@ -2730,10 +2754,9 @@ describe('Test axes', function() { expect(size.r).toBe(previousSize.r); expect(size.b).toBe(previousSize.b); expect(size.t).toBe(previousSize.t); - }) - .then(function() { - previousSize = Lib.extendDeep({}, gd._fullLayout._size); - return Plotly.relayout(gd, { 'yaxis.anchor': 'x' }); + + previousSize = Lib.extendDeep({}, size); + return Plotly.relayout(gd, {'yaxis.anchor': 'x'}); }) .then(function() { var size = gd._fullLayout._size; @@ -2741,9 +2764,8 @@ describe('Test axes', function() { expect(size.r).toBe(previousSize.r); expect(size.b).toBe(previousSize.b); expect(size.t).toBe(previousSize.t); - }) - .then(function() { - previousSize = Lib.extendDeep({}, gd._fullLayout._size); + + previousSize = Lib.extendDeep({}, size); return Plotly.relayout(gd, { 'yaxis.side': 'right', 'xaxis.side': 'top' @@ -2756,8 +2778,7 @@ describe('Test axes', function() { expect(size.r).toBe(previousSize.l); expect(size.b).toBe(initialSize.b); expect(size.t).toBe(previousSize.b); - }) - .then(function() { + return Plotly.relayout(gd, { 'xaxis.automargin': false, 'yaxis.automargin': false diff --git a/test/jasmine/tests/colorbar_test.js b/test/jasmine/tests/colorbar_test.js index 0c722327cb1..a4ff4de3c31 100644 --- a/test/jasmine/tests/colorbar_test.js +++ b/test/jasmine/tests/colorbar_test.js @@ -1,7 +1,11 @@ +var d3 = require('d3'); + var Plotly = require('@lib/index'); var Colorbar = require('@src/components/colorbar'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); +var failTest = require('../assets/fail_test'); +var assertPlotSize = require('../assets/custom_assertions').assertPlotSize; describe('Test colorbar:', function() { @@ -33,7 +37,7 @@ describe('Test colorbar:', function() { }); }); - describe('floating point limits', function() { + describe('drawing & editing', function() { var gd; beforeEach(function() { @@ -53,7 +57,241 @@ describe('Test colorbar:', function() { [9607345622458654.0, 9607345622458652.0, 9607345622458654.0, 9607345622458650.0, 9607345622458654.0, 9607345622458652.0, 9607345622458640.0, 9607345622458654.0], [9607345622458650.0, 9607345622458652.0, 9607345622458650.0, 9607345622458652.0, 9607345622458650.0, 9607345622458654.0, 9607345622458654.0, 9607345622458638.0] ]; - Plotly.newPlot(gd, [{type: 'heatmap', z: z}]).then(done); + Plotly.newPlot(gd, [{type: 'heatmap', z: z}]) + .catch(failTest) + .then(done); + }); + + // let heatmap stand in for all traces with trace.{showscale, colorbar} + // also test impliedEdits for colorbars at the trace root + it('can show and hide heatmap colorbars and sizes correctly with automargin', function(done) { + function assertCB(msg, present, expandedMarginR, expandedMarginT, cbTop, cbHeight) { + var colorbars = d3.select(gd).selectAll('.colorbar'); + expect(colorbars.size()).toBe(present ? 1 : 0); + + var cbbg = colorbars.selectAll('.colorbar .cbbg'); + + // check that the displayed object has the right size, + // not just that fullLayout._size changed + var plotSizeTest = {}; + if(expandedMarginR) plotSizeTest.widthLessThan = 400; + else plotSizeTest.width = 400; + + if(expandedMarginT) plotSizeTest.heightLessThan = 400; + else plotSizeTest.height = 400; + + assertPlotSize(plotSizeTest); + + if(present) { + if(!cbHeight) cbHeight = 400; + var bgHeight = +cbbg.attr('height'); + if(expandedMarginT) expect(bgHeight).toBeLessThan(cbHeight - 2); + else expect(bgHeight).toBeWithin(cbHeight, 2); + + var topShift = cbbg.node().getBoundingClientRect().top - gd.getBoundingClientRect().top; + expect(topShift).toBeWithin(cbTop, 2); + } + } + + var thickPx, lenFrac; + + Plotly.newPlot(gd, [{ + type: 'heatmap', + z: [[1, 10], [100, 1000]] + }], { + height: 500, + width: 500, + margin: {l: 50, r: 50, t: 50, b: 50} + }) + .then(function() { + assertCB('initial', true, true, false, 50); + + return Plotly.restyle(gd, {showscale: false}); + }) + .then(function() { + assertCB('hidden', false, false, false); + + return Plotly.restyle(gd, {showscale: true, 'colorbar.y': 0.8}); + }) + .then(function() { + assertCB('up high', true, true, true, 12); + + return Plotly.restyle(gd, {'colorbar.y': 0.7}); + }) + .then(function() { + assertCB('a little lower', true, true, true, 12); + + return Plotly.restyle(gd, { + 'colorbar.x': 0.7, + 'colorbar.y': 0.5, + 'colorbar.thickness': 50, + 'colorbar.len': 0.5 + }); + }) + .then(function() { + assertCB('mid-plot', true, false, false, 150, 200); + + thickPx = gd._fullData[0].colorbar.thickness; + lenFrac = gd._fullData[0].colorbar.len; + + return Plotly.restyle(gd, { + 'colorbar.x': 1.1, + 'colorbar.thicknessmode': 'fraction', + 'colorbar.lenmode': 'pixels' + }); + }) + .then(function() { + expect(gd._fullData[0].colorbar.thickness) + .toBeCloseTo(thickPx / 400, 5); + expect(gd._fullData[0].colorbar.len) + .toBeCloseTo(lenFrac * 400, 3); + + assertCB('changed size modes', true, true, false, 150, 200); + }) + .catch(failTest) + .then(done); + }); + + // scatter has trace.marker.{showscale, colorbar} + it('can show and hide scatter colorbars', function(done) { + function assertCB(present, expandedMargin) { + var colorbars = d3.select(gd).selectAll('.colorbar'); + expect(colorbars.size()).toBe(present ? 1 : 0); + + assertPlotSize(expandedMargin ? {widthLessThan: 400} : {width: 400}); + } + + Plotly.newPlot(gd, [{ + y: [1, 2, 3], + marker: {color: [1, 2, 3], showscale: true} + }], { + height: 500, + width: 500, + margin: {l: 50, r: 50, t: 50, b: 50} + }) + .then(function() { + assertCB(true, true); + + return Plotly.restyle(gd, {'marker.showscale': false}); + }) + .then(function() { + assertCB(false, false); + + return Plotly.restyle(gd, {'marker.showscale': true, 'marker.colorbar.x': 0.7}); + }) + .then(function() { + assertCB(true, false); + + return Plotly.restyle(gd, {'marker.colorbar.x': 1.1}); + }) + .then(function() { + assertCB(true, true); + }) + .catch(failTest) + .then(done); + }); + + // histogram colorbars could not be edited before + it('can show and hide scatter colorbars', function(done) { + function assertCB(present, expandedMargin) { + var colorbars = d3.select(gd).selectAll('.colorbar'); + expect(colorbars.size()).toBe(present ? 1 : 0); + + assertPlotSize(expandedMargin ? {widthLessThan: 400} : {width: 400}); + } + + Plotly.newPlot(gd, [{ + type: 'histogram', + x: [0, 1, 1, 2, 2, 2, 3, 3, 4], + xbins: {start: -1.5, end: 4.5, size: 1}, + marker: {color: [1, 2, 3, 4, 5, 6], showscale: true} + }], { + height: 500, + width: 500, + margin: {l: 50, r: 50, t: 50, b: 50} + }) + .then(function() { + assertCB(true, true); + + return Plotly.restyle(gd, {'marker.showscale': false}); + }) + .then(function() { + assertCB(false, false); + + return Plotly.restyle(gd, {'marker.showscale': true, 'marker.colorbar.x': 0.7}); + }) + .then(function() { + assertCB(true, false); + + return Plotly.restyle(gd, {'marker.colorbar.x': 1.1}); + }) + .then(function() { + assertCB(true, true); + }) + .catch(failTest) + .then(done); + }); + + // parcoords has trace.marker.{showscale, colorbar} + // also tests impliedEdits for colorbars in containers + it('can show and hide parcoords colorbars', function(done) { + function assertCB(present, expandedMargin) { + var colorbars = d3.select(gd).selectAll('.colorbar'); + expect(colorbars.size()).toBe(present ? 1 : 0); + + var yAxes = d3.select(gd).selectAll('.parcoords .y-axis'); + expect(yAxes.size()).toBe(2); + var transform = yAxes[0][1].getAttribute('transform'); + if(expandedMargin) expect(transform).not.toBe('translate(400, 0)'); + else expect(transform).toBe('translate(400, 0)'); + } + + var thickPx, lenFrac; + + Plotly.newPlot(gd, [{ + type: 'parcoords', + dimensions: [{values: [1, 2]}, {values: [1, 2]}], + line: {color: [1, 2], showscale: true} + }], { + height: 500, + width: 500, + margin: {l: 50, r: 50, t: 50, b: 50} + }) + .then(function() { + assertCB(true, true); + + return Plotly.restyle(gd, {'line.showscale': false}); + }) + .then(function() { + assertCB(false, false); + + return Plotly.restyle(gd, { + 'line.showscale': true, + 'line.colorbar.x': 0.7 + }); + }) + .then(function() { + assertCB(true, false); + + thickPx = gd._fullData[0].line.colorbar.thickness; + lenFrac = gd._fullData[0].line.colorbar.len; + + return Plotly.restyle(gd, { + 'line.colorbar.x': 1.1, + 'line.colorbar.thicknessmode': 'fraction', + 'line.colorbar.lenmode': 'pixels' + }); + }) + .then(function() { + expect(gd._fullData[0].line.colorbar.thickness) + .toBeCloseTo(thickPx / 400, 5); + expect(gd._fullData[0].line.colorbar.len) + .toBeCloseTo(lenFrac * 400, 3); + + assertCB(true, true); + }) + .catch(failTest) + .then(done); }); }); }); diff --git a/test/jasmine/tests/gl2d_click_test.js b/test/jasmine/tests/gl2d_click_test.js index 1bade67fc6a..512409242d7 100644 --- a/test/jasmine/tests/gl2d_click_test.js +++ b/test/jasmine/tests/gl2d_click_test.js @@ -124,7 +124,6 @@ describe('@gl @flaky Test hover and click interactions', function() { 'data', 'fullData', 'xaxis', 'yaxis' ]), 'event data keys'); - expect(typeof pt.data.uid).toBe('string', msg + ' - uid'); expect(pt.xaxis.domain.length).toBe(2, msg + ' - xaxis'); expect(pt.yaxis.domain.length).toBe(2, msg + ' - yaxis'); @@ -149,7 +148,7 @@ describe('@gl @flaky Test hover and click interactions', function() { return delay(100)() .then(_hover) .then(function(eventData) { - assertEventData(eventData, expected); + assertEventData(eventData, expected, opts.msg); var g = d3.select('g.hovertext'); if(g.node() === null) { diff --git a/test/jasmine/tests/gl2d_pointcloud_test.js b/test/jasmine/tests/gl2d_pointcloud_test.js index ebe0e414f8c..44320b9606f 100644 --- a/test/jasmine/tests/gl2d_pointcloud_test.js +++ b/test/jasmine/tests/gl2d_pointcloud_test.js @@ -138,12 +138,6 @@ var plotData = { } }; -function makePlot(gd, mock, done) { - return Plotly.plot(gd, mock.data, mock.layout) - .then(null, failTest) - .then(done); -} - describe('@gl pointcloud traces', function() { var gd; @@ -157,8 +151,10 @@ describe('@gl pointcloud traces', function() { destroyGraphDiv(); }); - it('render without raising an error', function(done) { - makePlot(gd, plotData, done); + it('renders without raising an error', function(done) { + Plotly.plot(gd, plotData) + .catch(failTest) + .then(done); }); it('should update properly', function(done) { @@ -171,10 +167,11 @@ describe('@gl pointcloud traces', function() { var yBaselineMins = [{val: 0, pad: 50, extrapad: false}]; var yBaselineMaxes = [{val: 9, pad: 50, extrapad: false}]; - Plotly.plot(gd, mock.data, mock.layout).then(function() { + Plotly.plot(gd, mock) + .then(function() { scene2d = gd._fullLayout._plots.xy._scene2d; - expect(scene2d.traces[mock.data[0].uid].type).toEqual('pointcloud'); + expect(scene2d.traces[gd._fullData[0].uid].type).toBe('pointcloud'); expect(scene2d.xaxis._min).toEqual(xBaselineMins); expect(scene2d.xaxis._max).toEqual(xBaselineMaxes); @@ -204,8 +201,8 @@ describe('@gl pointcloud traces', function() { }).then(function() { expect(scene2d.yaxis._min).toEqual(yBaselineMins); expect(scene2d.yaxis._max).toEqual(yBaselineMaxes); - - done(); - }); + }) + .catch(failTest) + .then(done); }); }); diff --git a/test/jasmine/tests/legend_test.js b/test/jasmine/tests/legend_test.js index b92df5bc529..871e01381c1 100644 --- a/test/jasmine/tests/legend_test.js +++ b/test/jasmine/tests/legend_test.js @@ -13,6 +13,7 @@ var failTest = require('../assets/fail_test'); var delay = require('../assets/delay'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); +var assertPlotSize = require('../assets/custom_assertions').assertPlotSize; describe('legend defaults', function() { 'use strict'; @@ -524,18 +525,32 @@ describe('legend relayout update', function() { afterEach(destroyGraphDiv); it('should hide and show the legend', function(done) { - var mockCopy = Lib.extendDeep({}, mock); + var mockCopy = Lib.extendDeep({}, mock, {layout: { + legend: {x: 1.1, xanchor: 'left'}, + margin: {l: 50, r: 50, pad: 0}, + width: 500 + }}); + Plotly.newPlot(gd, mockCopy.data, mockCopy.layout) .then(function() { expect(d3.selectAll('g.legend').size()).toBe(1); + // check that the margins changed + assertPlotSize({widthLessThan: 400}); return Plotly.relayout(gd, {showlegend: false}); }) .then(function() { expect(d3.selectAll('g.legend').size()).toBe(0); + assertPlotSize({width: 400}); return Plotly.relayout(gd, {showlegend: true}); }) .then(function() { expect(d3.selectAll('g.legend').size()).toBe(1); + assertPlotSize({widthLessThan: 400}); + return Plotly.relayout(gd, {'legend.x': 0.7}); + }) + .then(function() { + expect(d3.selectAll('g.legend').size()).toBe(1); + assertPlotSize({width: 400}); }) .catch(failTest) .then(done); diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index 0e370e06b7d..01cca99320c 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -520,7 +520,8 @@ describe('Test plot api', function() { 'doCamera', 'doAutoRangeAndConstraints', 'drawData', - 'finalDraw' + 'finalDraw', + 'drawMarginPushers' ]; var gd; @@ -1497,9 +1498,7 @@ describe('Test plot api', function() { it('should work when newIndices is undefined', function() { Plotly.addTraces(gd, [{'name': 'c'}, {'name': 'd'}]); expect(gd.data[2].name).toBeDefined(); - expect(gd.data[2].uid).toBeDefined(); expect(gd.data[3].name).toBeDefined(); - expect(gd.data[3].uid).toBeDefined(); expect(plotApi.redraw).toHaveBeenCalled(); expect(plotApi.moveTraces).not.toHaveBeenCalled(); }); @@ -1507,9 +1506,7 @@ describe('Test plot api', function() { it('should work when newIndices is defined', function() { Plotly.addTraces(gd, [{'name': 'c'}, {'name': 'd'}], [1, 3]); expect(gd.data[2].name).toBeDefined(); - expect(gd.data[2].uid).toBeDefined(); expect(gd.data[3].name).toBeDefined(); - expect(gd.data[3].uid).toBeDefined(); expect(plotApi.redraw).not.toHaveBeenCalled(); expect(plotApi.moveTraces).toHaveBeenCalledWith(gd, [-2, -1], [1, 3]); }); @@ -1517,9 +1514,7 @@ describe('Test plot api', function() { it('should work when newIndices has negative indices', function() { Plotly.addTraces(gd, [{'name': 'c'}, {'name': 'd'}], [-3, -1]); expect(gd.data[2].name).toBeDefined(); - expect(gd.data[2].uid).toBeDefined(); expect(gd.data[3].name).toBeDefined(); - expect(gd.data[3].uid).toBeDefined(); expect(plotApi.redraw).not.toHaveBeenCalled(); expect(plotApi.moveTraces).toHaveBeenCalledWith(gd, [-2, -1], [-3, -1]); }); @@ -1527,7 +1522,6 @@ describe('Test plot api', function() { it('should work when newIndices is an integer', function() { Plotly.addTraces(gd, {'name': 'c'}, 0); expect(gd.data[2].name).toBeDefined(); - expect(gd.data[2].uid).toBeDefined(); expect(plotApi.redraw).not.toHaveBeenCalled(); expect(plotApi.moveTraces).toHaveBeenCalledWith(gd, [-1], [0]); }); diff --git a/test/jasmine/tests/plots_test.js b/test/jasmine/tests/plots_test.js index 9fd68668f06..8e5b72fc5ba 100644 --- a/test/jasmine/tests/plots_test.js +++ b/test/jasmine/tests/plots_test.js @@ -37,6 +37,7 @@ describe('Test Plots', function() { type: 'contour', _empties: [1, 2, 3] }]; + oldFullData.forEach(function(trace) { trace._fullInput = trace; }); var oldFullLayout = { _plots: { xy: { plot: {} } }, diff --git a/test/jasmine/tests/range_selector_test.js b/test/jasmine/tests/range_selector_test.js index 3a29cabac23..16c59a23466 100644 --- a/test/jasmine/tests/range_selector_test.js +++ b/test/jasmine/tests/range_selector_test.js @@ -7,9 +7,11 @@ var Lib = require('@src/lib'); var Color = require('@src/components/color'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); +var failTest = require('../assets/fail_test'); var getRectCenter = require('../assets/get_rect_center'); var mouseEvent = require('../assets/mouse_event'); var setConvert = require('@src/plots/cartesian/set_convert'); +var assertPlotSize = require('../assets/custom_assertions').assertPlotSize; describe('range selector defaults:', function() { @@ -461,7 +463,9 @@ describe('range selector interactions:', function() { gd = createGraphDiv(); mockCopy = Lib.extendDeep({}, mock); - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + Plotly.plot(gd, mockCopy.data, mockCopy.layout) + .catch(failTest) + .then(done); }); afterEach(destroyGraphDiv); @@ -495,9 +499,9 @@ describe('range selector interactions:', function() { Plotly.relayout(gd, 'xaxis.rangeselector.visible', false).then(function() { assertNodeCount('.rangeselector', 0); assertNodeCount('.button', 0); - done(); - }); - + }) + .catch(failTest) + .then(done); }); it('should be able to remove button(s) on `relayout`', function(done) { @@ -511,9 +515,9 @@ describe('range selector interactions:', function() { return Plotly.relayout(gd, 'xaxis.rangeselector.buttons[1]', 'remove'); }).then(function() { assertNodeCount('.button', len - 2); - - done(); - }); + }) + .catch(failTest) + .then(done); }); it('should be able to change its style on `relayout`', function(done) { @@ -527,9 +531,9 @@ describe('range selector interactions:', function() { return Plotly.relayout(gd, prefix + 'activecolor', 'blue'); }).then(function() { checkButtonColor('rgb(255, 0, 0)', 'rgb(0, 0, 255)'); - - done(); - }); + }) + .catch(failTest) + .then(done); }); it('should update range and active button when clicked', function() { @@ -589,8 +593,56 @@ describe('range selector interactions:', function() { // 'all' should be after an autoscale checkActiveButton(buttons.size() - 1, 'back to all'); + }) + .catch(failTest) + .then(done); + }); +}); - done(); - }); +describe('range selector automargin', function() { + 'use strict'; + + var mock = require('@mocks/range_selector.json'); + + var gd, mockCopy; + + beforeEach(function(done) { + gd = createGraphDiv(); + mockCopy = Lib.extendDeep({}, mock, {layout: { + width: 500, + height: 500, + margin: {l: 50, r: 50, t: 100, b: 100} + }}); + + Plotly.plot(gd, mockCopy.data, mockCopy.layout) + .catch(failTest) + .then(done); }); + + afterEach(destroyGraphDiv); + + it('updates automargin when hiding, showing, or moving', function(done) { + assertPlotSize({width: 400, height: 300}, 'initial'); + + Plotly.relayout(gd, { + 'xaxis.rangeselector.y': 1.3, + 'xaxis.rangeselector.xanchor': 'center' + }) + .then(function() { + assertPlotSize({widthLessThan: 400, heightLessThan: 300}, 'moved'); + + return Plotly.relayout(gd, {'xaxis.rangeselector.visible': false}); + }) + .then(function() { + assertPlotSize({width: 400, height: 300}, 'hidden'); + + return Plotly.relayout(gd, {'xaxis.rangeselector.visible': true}); + }) + .then(function() { + assertPlotSize({widthLessThan: 400, heightLessThan: 300}, 'reshow'); + }) + .catch(failTest) + .then(done); + }); + }); diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index 4c725d5526d..9eea7bbecdf 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -13,6 +13,7 @@ var destroyGraphDiv = require('../assets/destroy_graph_div'); var mouseEvent = require('../assets/mouse_event'); var supplyAllDefaults = require('../assets/supply_defaults'); var failTest = require('../assets/fail_test'); +var assertPlotSize = require('../assets/custom_assertions').assertPlotSize; var TOL = 6; @@ -363,31 +364,41 @@ describe('Rangeslider visibility property', function() { afterEach(destroyGraphDiv); + function defaultLayout(opts) { + return Lib.extendDeep({ + width: 500, + height: 600, + margin: {l: 50, r: 50, t: 100, b: 100} + }, opts || {}); + } + it('should not add the slider to the DOM by default', function(done) { - Plotly.plot(gd, [{ x: [1, 2, 3], y: [2, 3, 4] }], {}) + Plotly.plot(gd, [{ x: [1, 2, 3], y: [2, 3, 4] }], defaultLayout()) .then(function() { var rangeSlider = getRangeSlider(); expect(rangeSlider).not.toBeDefined(); + assertPlotSize({height: 400}); }) .catch(failTest) .then(done); }); it('should add the slider if rangeslider is set to anything', function(done) { - Plotly.plot(gd, [{ x: [1, 2, 3], y: [2, 3, 4] }], {}) + Plotly.plot(gd, [{ x: [1, 2, 3], y: [2, 3, 4] }], defaultLayout()) .then(function() { return Plotly.relayout(gd, 'xaxis.rangeslider', 'exists'); }) .then(function() { var rangeSlider = getRangeSlider(); expect(rangeSlider).toBeDefined(); + assertPlotSize({heightLessThan: 400}); }) .catch(failTest) .then(done); }); it('should add the slider if visible changed to `true`', function(done) { - Plotly.plot(gd, [{ x: [1, 2, 3], y: [2, 3, 4] }], {}) + Plotly.plot(gd, [{ x: [1, 2, 3], y: [2, 3, 4] }], defaultLayout()) .then(function() { return Plotly.relayout(gd, 'xaxis.rangeslider.visible', true); }) @@ -395,6 +406,7 @@ describe('Rangeslider visibility property', function() { var rangeSlider = getRangeSlider(); expect(rangeSlider).toBeDefined(); expect(countRangeSliderClipPaths()).toEqual(1); + assertPlotSize({heightLessThan: 400}); }) .catch(failTest) .then(done); @@ -404,11 +416,11 @@ describe('Rangeslider visibility property', function() { Plotly.plot(gd, [{ x: [1, 2, 3], y: [2, 3, 4] - }], { + }], defaultLayout({ xaxis: { rangeslider: { visible: true } } - }) + })) .then(function() { return Plotly.relayout(gd, 'xaxis.rangeslider.visible', false); }) @@ -416,6 +428,7 @@ describe('Rangeslider visibility property', function() { var rangeSlider = getRangeSlider(); expect(rangeSlider).not.toBeDefined(); expect(countRangeSliderClipPaths()).toEqual(0); + assertPlotSize({height: 400}); }) .catch(failTest) .then(done); @@ -434,11 +447,11 @@ describe('Rangeslider visibility property', function() { type: 'bar', x: [1, 2, 3], y: [2, 5, 2] - }], { + }], defaultLayout({ xaxis: { rangeslider: { visible: true } } - }) + })) .then(function() { expect(count('g.scatterlayer > g.trace')).toEqual(1); expect(count('g.barlayer > g.trace')).toEqual(1); diff --git a/test/jasmine/tests/sliders_test.js b/test/jasmine/tests/sliders_test.js index 5d317fcf8a9..84e165ce671 100644 --- a/test/jasmine/tests/sliders_test.js +++ b/test/jasmine/tests/sliders_test.js @@ -6,7 +6,9 @@ var Plotly = require('@lib'); var Lib = require('@src/lib'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); -var fail = require('../assets/fail_test'); +var failTest = require('../assets/fail_test'); +var delay = require('../assets/delay'); +var assertPlotSize = require('../assets/custom_assertions').assertPlotSize; describe('sliders defaults', function() { 'use strict'; @@ -166,7 +168,7 @@ describe('sliders defaults', function() { }); }); - it('allow the `skip` method', function() { + it('allows the `skip` method', function() { layoutIn.sliders = [{ steps: [{ method: 'skip', @@ -295,7 +297,7 @@ describe('ugly internal manipulation of steps', function() { // The selected option no longer exists, so confirm it's // been fixed during the process of updating/drawing it: expect(gd._fullLayout.sliders[0].active).toEqual(0); - }).catch(fail).then(done); + }).catch(failTest).then(done); }); }); @@ -310,7 +312,7 @@ describe('sliders interactions', function() { beforeEach(function(done) { gd = createGraphDiv(); - mockCopy = Lib.extendDeep({}, mock); + mockCopy = Lib.extendDeep({}, mock, {layout: {sliders: [{x: 0.25}, {}]}}); Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); }); @@ -320,15 +322,48 @@ describe('sliders interactions', function() { destroyGraphDiv(); }); + it('positions sliders repeatably when they push margins', function(done) { + function checkPositions(msg) { + d3.select(gd).selectAll('.slider-group').each(function(d, i) { + var sliderBB = this.getBoundingClientRect(); + var gdBB = gd.getBoundingClientRect(); + if(i === 0) { + expect(sliderBB.left - gdBB.left) + .toBeWithin(12, 3, 'left: ' + msg); + } + else { + expect(gdBB.bottom - sliderBB.bottom) + .toBeWithin(8, 3, 'bottom: ' + msg); + } + }); + } + + checkPositions('initial'); + + Plotly.relayout(gd, {'sliders[0].x': 0.35, 'sliders[1].y': -0.3}) + .then(function() { + checkPositions('increased left & bottom'); + + return Plotly.relayout(gd, {'sliders[0].x': 0.1, 'sliders[1].y': -0.1}); + }) + .then(function() { + checkPositions('back to original'); + }) + .catch(failTest) + .then(done); + }); + it('should draw only visible sliders', function(done) { expect(gd._fullLayout._pushmargin['slider-0']).toBeDefined(); expect(gd._fullLayout._pushmargin['slider-1']).toBeDefined(); + assertPlotSize({heightLessThan: 270}, 'initial'); Plotly.relayout(gd, 'sliders[0].visible', false).then(function() { assertNodeCount('.' + constants.groupClassName, 1); expect(gd._fullLayout._pushmargin['slider-0']).toBeUndefined(); expect(gd._fullLayout._pushmargin['slider-1']).toBeDefined(); expect(gd.layout.sliders.length).toEqual(2); + assertPlotSize({heightLessThan: 270}, 'hide 0'); return Plotly.relayout(gd, 'sliders[1]', null); }) @@ -337,6 +372,7 @@ describe('sliders interactions', function() { expect(gd._fullLayout._pushmargin['slider-0']).toBeUndefined(); expect(gd._fullLayout._pushmargin['slider-1']).toBeUndefined(); expect(gd.layout.sliders.length).toEqual(1); + assertPlotSize({height: 270}, 'delete 1'); return Plotly.relayout(gd, { 'sliders[0].visible': true, @@ -346,6 +382,7 @@ describe('sliders interactions', function() { assertNodeCount('.' + constants.groupClassName, 1); expect(gd._fullLayout._pushmargin['slider-0']).toBeDefined(); expect(gd._fullLayout._pushmargin['slider-1']).toBeUndefined(); + assertPlotSize({heightLessThan: 270}, 'reshow 0'); return Plotly.relayout(gd, { 'sliders[1]': { @@ -366,7 +403,8 @@ describe('sliders interactions', function() { expect(gd._fullLayout._pushmargin['slider-0']).toBeDefined(); expect(gd._fullLayout._pushmargin['slider-1']).toBeDefined(); }) - .catch(fail).then(done); + .catch(failTest) + .then(done); }); it('should respond to mouse clicks', function(done) { @@ -396,7 +434,8 @@ describe('sliders interactions', function() { var mousemoveFill = firstGrip.node().style.fill; expect(mousemoveFill).toEqual(mousedownFill); - setTimeout(function() { + delay(100)() + .then(function() { expect(mockCopy.layout.sliders[0].active).toEqual(0); gd.dispatchEvent(new MouseEvent('mouseup')); @@ -404,9 +443,9 @@ describe('sliders interactions', function() { var mouseupFill = firstGrip.node().style.fill; expect(mouseupFill).toEqual(originalFill); expect(mockCopy.layout.sliders[0].active).toEqual(0); - - done(); - }, 100); + }) + .catch(failTest) + .then(done); }); it('should issue events on interaction', function(done) { @@ -427,15 +466,14 @@ describe('sliders interactions', function() { cntEnd++; }); - function assertEventCounts(starts, interactions, noninteractions, ends) { - expect( - [cntStart, cntInteraction, cntNonInteraction, cntEnd] - ).toEqual( - [starts, interactions, noninteractions, ends] - ); + function assertEventCounts(starts, interactions, noninteractions, ends, msg) { + expect(cntStart).toBe(starts, 'starts: ' + msg); + expect(cntInteraction).toBe(interactions, 'interactions: ' + msg); + expect(cntNonInteraction).toBe(noninteractions, 'noninteractions: ' + msg); + expect(cntEnd).toBe(ends, 'ends: ' + msg); } - assertEventCounts(0, 0, 0, 0); + assertEventCounts(0, 0, 0, 0, 'initial'); var firstGroup = gd._fullLayout._infolayer.select('.' + constants.railTouchRectClass); var railNode = firstGroup.node(); @@ -447,30 +485,31 @@ describe('sliders interactions', function() { clientX: touchRect.left + touchRect.width - 5, })); - setTimeout(function() { + delay(50)() + .then(function() { // One slider received a mousedown, one received an interaction, and one received a change: - assertEventCounts(1, 1, 1, 0); + assertEventCounts(1, 1, 1, 0, 'mousedown'); // Drag to the left side: gd.dispatchEvent(new MouseEvent('mousemove', { clientY: touchRect.top + 5, clientX: touchRect.left + 5, })); + }) + .then(delay(50)) + .then(function() { + // On move, now to changes for the each slider, and no ends: + assertEventCounts(1, 2, 2, 0, 'mousemove'); - setTimeout(function() { - // On move, now to changes for the each slider, and no ends: - assertEventCounts(1, 2, 2, 0); - - gd.dispatchEvent(new MouseEvent('mouseup')); - - setTimeout(function() { - // Now an end: - assertEventCounts(1, 2, 2, 1); - - done(); - }, 50); - }, 50); - }, 50); + gd.dispatchEvent(new MouseEvent('mouseup')); + }) + .then(delay(50)) + .then(function() { + // Now an end: + assertEventCounts(1, 2, 2, 1, 'mouseup'); + }) + .catch(failTest) + .then(done); }); function assertNodeCount(query, cnt) { diff --git a/test/jasmine/tests/transform_filter_test.js b/test/jasmine/tests/transform_filter_test.js index 4a9eb4e27c5..e518d9080f2 100644 --- a/test/jasmine/tests/transform_filter_test.js +++ b/test/jasmine/tests/transform_filter_test.js @@ -8,6 +8,7 @@ var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var customAssertions = require('../assets/custom_assertions'); var supplyAllDefaults = require('../assets/supply_defaults'); +var failTest = require('../assets/fail_test'); var assertDims = customAssertions.assertDims; var assertStyle = customAssertions.assertStyle; @@ -1078,11 +1079,11 @@ describe('filter transforms interactions', function() { Plotly.plot(createGraphDiv(), data).then(function(gd) { assertDims([3]); - var uid = data[0].uid; + var uid = gd._fullData[0]._fullInput.uid; expect(gd._fullData[0].uid).toEqual(uid + '0'); - - done(); - }); + }) + .catch(failTest) + .then(done); }); it('Plotly.restyle should work', function(done) { @@ -1095,11 +1096,11 @@ describe('filter transforms interactions', function() { var uid; function assertUid(gd) { expect(gd._fullData[0].uid) - .toEqual(uid + '0', 'should preserve uid on restyle'); + .toBe(uid + '0', 'should preserve uid on restyle'); } Plotly.plot(gd, data).then(function() { - uid = gd.data[0].uid; + uid = gd._fullData[0]._fullInput.uid; expect(gd._fullData[0].marker.color).toEqual('red'); assertUid(gd); @@ -1127,9 +1128,9 @@ describe('filter transforms interactions', function() { expect(gd._fullLayout.xaxis.range).toBeCloseToArray([2, 4]); expect(gd._fullLayout.yaxis.range).toBeCloseToArray([0, 2]); - - done(); - }); + }) + .catch(failTest) + .then(done); }); it('Plotly.extendTraces should work', function(done) { @@ -1152,9 +1153,9 @@ describe('filter transforms interactions', function() { expect(gd._fullData[0].x.length).toEqual(5); assertDims([5]); - - done(); - }); + }) + .catch(failTest) + .then(done); }); it('Plotly.deleteTraces should work', function(done) { @@ -1172,9 +1173,9 @@ describe('filter transforms interactions', function() { return Plotly.deleteTraces(gd, [0]); }).then(function() { assertDims([]); - - done(); - }); + }) + .catch(failTest) + .then(done); }); @@ -1197,9 +1198,9 @@ describe('filter transforms interactions', function() { return Plotly.restyle(gd, 'visible', [true, true], [0, 1]); }).then(function() { assertDims([3, 4]); - - done(); - }); + }) + .catch(failTest) + .then(done); }); it('zooming in/out should not change filtered data', function(done) { @@ -1225,6 +1226,7 @@ describe('filter transforms interactions', function() { expect(gd.calcdata[0].map(getTx)).toEqual(['e', 'f', 'g']); expect(gd.calcdata[1].map(getTx)).toEqual(['D', 'E', 'F', 'G']); }) + .catch(failTest) .then(done); }); @@ -1268,6 +1270,7 @@ describe('filter transforms interactions', function() { expect(gd._fullLayout.xaxis._categories).toEqual(['i']); expect(gd._fullLayout.yaxis._categories).toEqual([]); }) + .catch(failTest) .then(done); }); diff --git a/test/jasmine/tests/transform_groupby_test.js b/test/jasmine/tests/transform_groupby_test.js index b9170ca6c01..2c838336973 100644 --- a/test/jasmine/tests/transform_groupby_test.js +++ b/test/jasmine/tests/transform_groupby_test.js @@ -15,7 +15,8 @@ function supplyDataDefaults(dataIn, dataOut) { return Plots.supplyDataDefaults(dataIn, dataOut, {}, { _subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']}, _modules: [], - _basePlotModules: [] + _basePlotModules: [], + _traceUids: dataIn.map(function() { return Lib.randstr(); }) }); } diff --git a/test/jasmine/tests/transform_multi_test.js b/test/jasmine/tests/transform_multi_test.js index 71b1d16a001..e9c04247329 100644 --- a/test/jasmine/tests/transform_multi_test.js +++ b/test/jasmine/tests/transform_multi_test.js @@ -18,7 +18,8 @@ var mockFullLayout = { _basePlotModules: [], _has: function() {}, _dfltTitle: {x: 'xxx', y: 'yyy'}, - _requestRangeslider: {} + _requestRangeslider: {}, + _traceUids: [] }; diff --git a/test/jasmine/tests/updatemenus_test.js b/test/jasmine/tests/updatemenus_test.js index 4e90579475d..9b7d64e0361 100644 --- a/test/jasmine/tests/updatemenus_test.js +++ b/test/jasmine/tests/updatemenus_test.js @@ -343,23 +343,29 @@ describe('update menus interactions', function() { destroyGraphDiv(); }); + function assertPushMargins(specs) { + specs.forEach(function(val, i) { + var push = gd._fullLayout._pushmargin['updatemenu-' + i]; + if(val) expect(push).toBeDefined(i); + else expect(push).toBeUndefined(i); + }); + } + it('should draw only visible menus', function(done) { var initialUM1 = Lib.extendDeep({}, gd.layout.updatemenus[1]); assertMenus([0, 0]); - expect(gd._fullLayout._pushmargin['updatemenu-0']).toBeDefined(); - expect(gd._fullLayout._pushmargin['updatemenu-1']).toBeDefined(); + assertPushMargins([true, true]); - Plotly.relayout(gd, 'updatemenus[0].visible', false).then(function() { + Plotly.relayout(gd, 'updatemenus[0].visible', false) + .then(function() { assertMenus([0]); - expect(gd._fullLayout._pushmargin['updatemenu-0']).toBeUndefined(); - expect(gd._fullLayout._pushmargin['updatemenu-1']).toBeDefined(); + assertPushMargins([false, true]); return Plotly.relayout(gd, 'updatemenus[1]', null); }) .then(function() { assertNodeCount('.' + constants.containerClassName, 0); - expect(gd._fullLayout._pushmargin['updatemenu-0']).toBeUndefined(); - expect(gd._fullLayout._pushmargin['updatemenu-1']).toBeUndefined(); + assertPushMargins([false, false]); return Plotly.relayout(gd, { 'updatemenus[0].visible': true, @@ -368,8 +374,7 @@ describe('update menus interactions', function() { }) .then(function() { assertMenus([0, 0]); - expect(gd._fullLayout._pushmargin['updatemenu-0']).toBeDefined(); - expect(gd._fullLayout._pushmargin['updatemenu-1']).toBeDefined(); + assertPushMargins([true, true]); return Plotly.relayout(gd, { 'updatemenus[0].visible': false, @@ -378,8 +383,7 @@ describe('update menus interactions', function() { }) .then(function() { assertNodeCount('.' + constants.containerClassName, 0); - expect(gd._fullLayout._pushmargin['updatemenu-0']).toBeUndefined(); - expect(gd._fullLayout._pushmargin['updatemenu-1']).toBeUndefined(); + assertPushMargins([false, false]); return Plotly.relayout(gd, { 'updatemenus[2]': { @@ -392,17 +396,13 @@ describe('update menus interactions', function() { }) .then(function() { assertMenus([0]); - expect(gd._fullLayout._pushmargin['updatemenu-0']).toBeUndefined(); - expect(gd._fullLayout._pushmargin['updatemenu-1']).toBeUndefined(); - expect(gd._fullLayout._pushmargin['updatemenu-2']).toBeDefined(); + assertPushMargins([false, false, true]); return Plotly.relayout(gd, 'updatemenus[0].visible', true); }) .then(function() { assertMenus([0, 0]); - expect(gd._fullLayout._pushmargin['updatemenu-0']).toBeDefined(); - expect(gd._fullLayout._pushmargin['updatemenu-1']).toBeUndefined(); - expect(gd._fullLayout._pushmargin['updatemenu-2']).toBeDefined(); + assertPushMargins([true, false, true]); expect(gd.layout.updatemenus.length).toEqual(3); return Plotly.relayout(gd, 'updatemenus[0]', null); @@ -410,11 +410,13 @@ describe('update menus interactions', function() { .then(function() { assertMenus([0]); expect(gd.layout.updatemenus.length).toEqual(2); + assertPushMargins([false, true, false]); return Plotly.relayout(gd, 'updatemenus', null); }) .then(function() { expect(gd.layout.updatemenus).toBeUndefined(); + assertPushMargins([false, false, false]); }) .then(done);