diff --git a/devtools/test_dashboard/devtools.js b/devtools/test_dashboard/devtools.js index 0922d66dd49..b1a0242dd20 100644 --- a/devtools/test_dashboard/devtools.js +++ b/devtools/test_dashboard/devtools.js @@ -202,7 +202,7 @@ function searchMocks(e) { results.forEach(function(r) { var result = document.createElement('span'); - result.className = 'search-result'; + result.className = getResultClass(r.name); result.innerText = r.name; result.addEventListener('click', function() { @@ -212,6 +212,10 @@ function searchMocks(e) { // Clear plots and plot selected. Tabs.purge(); Tabs.plotMock(mockName); + + mocksList.querySelectorAll('span').forEach(function(el) { + el.className = getResultClass(el.innerText); + }); }); mocksList.appendChild(result); @@ -222,8 +226,16 @@ function searchMocks(e) { }); } +function getNameFromHash() { + return window.location.hash.replace(/^#/, ''); +} + +function getResultClass(name) { + return 'search-result' + (getNameFromHash() === name ? ' search-result__selected' : ''); +} + function plotFromHash() { - var initialMock = window.location.hash.replace(/^#/, ''); + var initialMock = getNameFromHash(); if(initialMock.length > 0) { Tabs.plotMock(initialMock); diff --git a/devtools/test_dashboard/style.css b/devtools/test_dashboard/style.css index 524c88e7df1..bea18056312 100644 --- a/devtools/test_dashboard/style.css +++ b/devtools/test_dashboard/style.css @@ -56,6 +56,9 @@ header span{ color: #fff; background-color: #4983EC; } +.search-result__selected{ + background-color: #DDDDEE; +} #plots{ overflow: scroll; } diff --git a/src/components/annotations/attributes.js b/src/components/annotations/attributes.js index a5298f29b42..8559c240f68 100644 --- a/src/components/annotations/attributes.js +++ b/src/components/annotations/attributes.js @@ -20,7 +20,7 @@ module.exports = { valType: 'boolean', role: 'info', dflt: true, - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', description: [ 'Determines whether or not this annotation is visible.' ].join(' ') @@ -29,7 +29,7 @@ module.exports = { text: { valType: 'string', role: 'info', - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', description: [ 'Sets the text associated with this annotation.', 'Plotly uses a subset of HTML tags to do things like', @@ -42,14 +42,14 @@ module.exports = { valType: 'angle', dflt: 0, role: 'style', - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', description: [ 'Sets the angle at which the `text` is drawn', 'with respect to the horizontal.' ].join(' ') }, font: fontAttrs({ - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', colorEditType: 'arraydraw', description: 'Sets the annotation text font.' }), @@ -58,7 +58,7 @@ module.exports = { min: 1, dflt: null, role: 'style', - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', description: [ 'Sets an explicit width for the text box. null (default) lets the', 'text set the box width. Wider text will be clipped.', @@ -70,7 +70,7 @@ module.exports = { min: 1, dflt: null, role: 'style', - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', description: [ 'Sets an explicit height for the text box. null (default) lets the', 'text set the box height. Taller text will be clipped.' @@ -131,7 +131,7 @@ module.exports = { min: 0, dflt: 1, role: 'style', - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', description: [ 'Sets the padding (in px) between the `text`', 'and the enclosing border.' @@ -142,7 +142,7 @@ module.exports = { min: 0, dflt: 1, role: 'style', - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', description: [ 'Sets the width (in px) of the border enclosing', 'the annotation `text`.' @@ -153,7 +153,7 @@ module.exports = { valType: 'boolean', dflt: true, role: 'style', - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', description: [ 'Determines whether or not the annotation is drawn with an arrow.', 'If *true*, `text` is placed near the arrow\'s tail.', @@ -198,7 +198,7 @@ module.exports = { min: 0.3, dflt: 1, role: 'style', - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', description: [ 'Sets the size of the end annotation arrow head, relative to `arrowwidth`.', 'A value of 1 (default) gives a head about 3x as wide as the line.' @@ -209,7 +209,7 @@ module.exports = { min: 0.3, dflt: 1, role: 'style', - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', description: [ 'Sets the size of the start annotation arrow head, relative to `arrowwidth`.', 'A value of 1 (default) gives a head about 3x as wide as the line.' @@ -219,7 +219,7 @@ module.exports = { valType: 'number', min: 0.1, role: 'style', - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', description: 'Sets the width (in px) of annotation arrow line.' }, standoff: { @@ -227,7 +227,7 @@ module.exports = { min: 0, dflt: 0, role: 'style', - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', description: [ 'Sets a distance, in pixels, to move the end arrowhead away from the', 'position it is pointing at, for example to point at the edge of', @@ -241,7 +241,7 @@ module.exports = { min: 0, dflt: 0, role: 'style', - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', description: [ 'Sets a distance, in pixels, to move the start arrowhead away from the', 'position it is pointing at, for example to point at the edge of', @@ -253,7 +253,7 @@ module.exports = { ax: { valType: 'any', role: 'info', - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', description: [ 'Sets the x component of the arrow tail about the arrow head.', 'If `axref` is `pixel`, a positive (negative) ', @@ -266,7 +266,7 @@ module.exports = { ay: { valType: 'any', role: 'info', - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', description: [ 'Sets the y component of the arrow tail about the arrow head.', 'If `ayref` is `pixel`, a positive (negative) ', @@ -333,7 +333,7 @@ module.exports = { x: { valType: 'any', role: 'info', - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', description: [ 'Sets the annotation\'s x position.', 'If the axis `type` is *log*, then you must take the', @@ -351,7 +351,7 @@ module.exports = { values: ['auto', 'left', 'center', 'right'], dflt: 'auto', role: 'info', - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', description: [ 'Sets the text box\'s horizontal position anchor', 'This anchor binds the `x` position to the *left*, *center*', @@ -370,7 +370,7 @@ module.exports = { valType: 'number', dflt: 0, role: 'style', - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', description: [ 'Shifts the position of the whole annotation and arrow to the', 'right (positive) or left (negative) by this many pixels.' @@ -396,7 +396,7 @@ module.exports = { y: { valType: 'any', role: 'info', - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', description: [ 'Sets the annotation\'s y position.', 'If the axis `type` is *log*, then you must take the', @@ -414,7 +414,7 @@ module.exports = { values: ['auto', 'top', 'middle', 'bottom'], dflt: 'auto', role: 'info', - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', description: [ 'Sets the text box\'s vertical position anchor', 'This anchor binds the `y` position to the *top*, *middle*', @@ -433,7 +433,7 @@ module.exports = { valType: 'number', dflt: 0, role: 'style', - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', description: [ 'Shifts the position of the whole annotation and arrow up', '(positive) or down (negative) by this many pixels.' diff --git a/src/components/annotations3d/convert.js b/src/components/annotations3d/convert.js index a4ba3f1d9a7..89d7c6ceb39 100644 --- a/src/components/annotations3d/convert.js +++ b/src/components/annotations3d/convert.js @@ -50,7 +50,7 @@ function mockAnnAxes(ann, scene) { Axes.setConvert(ann._xa); ann._xa._offset = size.l + domain.x[0] * size.w; ann._xa.l2p = function() { - return 0.5 * (1 + ann.pdata[0] / ann.pdata[3]) * size.w * (domain.x[1] - domain.x[0]); + return 0.5 * (1 + ann._pdata[0] / ann._pdata[3]) * size.w * (domain.x[1] - domain.x[0]); }; ann._ya = {}; @@ -58,6 +58,6 @@ function mockAnnAxes(ann, scene) { Axes.setConvert(ann._ya); ann._ya._offset = size.t + (1 - domain.y[1]) * size.h; ann._ya.l2p = function() { - return 0.5 * (1 - ann.pdata[1] / ann.pdata[3]) * size.h * (domain.y[1] - domain.y[0]); + return 0.5 * (1 - ann._pdata[1] / ann._pdata[3]) * size.h * (domain.y[1] - domain.y[0]); }; } diff --git a/src/components/annotations3d/draw.js b/src/components/annotations3d/draw.js index 56bc1af21b4..e1600bb7c24 100644 --- a/src/components/annotations3d/draw.js +++ b/src/components/annotations3d/draw.js @@ -38,7 +38,7 @@ module.exports = function draw(scene) { .select('.annotation-' + scene.id + '[data-index="' + i + '"]') .remove(); } else { - ann.pdata = project(scene.glplot.cameraParams, [ + ann._pdata = project(scene.glplot.cameraParams, [ fullSceneLayout.xaxis.r2l(ann.x) * dataScale[0], fullSceneLayout.yaxis.r2l(ann.y) * dataScale[1], fullSceneLayout.zaxis.r2l(ann.z) * dataScale[2] diff --git a/src/components/legend/defaults.js b/src/components/legend/defaults.js index cff1522de71..8dbf3cf8e9c 100644 --- a/src/components/legend/defaults.js +++ b/src/components/legend/defaults.js @@ -18,15 +18,13 @@ var helpers = require('./helpers'); module.exports = function legendDefaults(layoutIn, layoutOut, fullData) { - var containerIn = layoutIn.legend || {}, - containerOut = layoutOut.legend = {}; + var containerIn = layoutIn.legend || {}; + var containerOut = {}; - var visibleTraces = 0, - defaultOrder = 'normal', - defaultX, - defaultY, - defaultXAnchor, - defaultYAnchor; + var visibleTraces = 0; + var defaultOrder = 'normal'; + + var defaultX, defaultY, defaultXAnchor, defaultYAnchor; for(var i = 0; i < fullData.length; i++) { var trace = fullData[i]; @@ -58,6 +56,8 @@ module.exports = function legendDefaults(layoutIn, layoutOut, fullData) { if(showLegend === false) return; + layoutOut.legend = containerOut; + coerce('bgcolor', layoutOut.paper_bgcolor); coerce('bordercolor'); coerce('borderwidth'); diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index 6c6d6a54d86..3ba1a94194b 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -22,7 +22,10 @@ var handleClick = require('./handle_click'); var constants = require('./constants'); var interactConstants = require('../../constants/interactions'); -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 getLegendData = require('./get_legend_data'); var style = require('./style'); @@ -141,7 +144,7 @@ module.exports = function draw(gd) { computeLegendDimensions(gd, groups, traces); - if(opts.height > lyMax) { + if(opts._height > lyMax) { // If the legend doesn't fit in the plot area, // do not expand the vertical margins. expandHorizontalMargin(gd); @@ -157,21 +160,21 @@ module.exports = function draw(gd) { ly = gs.t + gs.h * (1 - opts.y); if(anchorUtils.isRightAnchor(opts)) { - lx -= opts.width; + lx -= opts._width; } else if(anchorUtils.isCenterAnchor(opts)) { - lx -= opts.width / 2; + lx -= opts._width / 2; } if(anchorUtils.isBottomAnchor(opts)) { - ly -= opts.height; + ly -= opts._height; } else if(anchorUtils.isMiddleAnchor(opts)) { - ly -= opts.height / 2; + ly -= opts._height / 2; } // Make sure the legend left and right sides are visible - var legendWidth = opts.width, + var legendWidth = opts._width, legendWidthMax = gs.w; if(legendWidth > legendWidthMax) { @@ -181,13 +184,13 @@ module.exports = function draw(gd) { else { if(lx + legendWidth > lxMax) lx = lxMax - legendWidth; if(lx < lxMin) lx = lxMin; - legendWidth = Math.min(lxMax - lx, opts.width); + legendWidth = Math.min(lxMax - lx, opts._width); } // Make sure the legend top and bottom are visible // (legends with a scroll bar are not allowed to stretch beyond the extended // margins) - var legendHeight = opts.height, + var legendHeight = opts._height, legendHeightMax = gs.h; if(legendHeight > legendHeightMax) { @@ -197,7 +200,7 @@ module.exports = function draw(gd) { else { if(ly + legendHeight > lyMax) ly = lyMax - legendHeight; if(ly < lyMin) ly = lyMin; - legendHeight = Math.min(lyMax - ly, opts.height); + legendHeight = Math.min(lyMax - ly, opts._height); } // Set size and position of all the elements that make up a legend: @@ -207,11 +210,11 @@ module.exports = function draw(gd) { var scrollBarYMax = legendHeight - constants.scrollBarHeight - 2 * constants.scrollBarMargin, - scrollBoxYMax = opts.height - legendHeight, + scrollBoxYMax = opts._height - legendHeight, scrollBarY, scrollBoxY; - if(opts.height <= legendHeight || gd._context.staticPlot) { + if(opts._height <= legendHeight || gd._context.staticPlot) { // if scrollbar should not be shown. bg.attr({ width: legendWidth - opts.borderwidth, @@ -533,8 +536,8 @@ function computeLegendDimensions(gd, groups, traces) { var extraWidth = 0; - opts.width = 0; - opts.height = 0; + opts._width = 0; + opts._height = 0; if(helpers.isVertical(opts)) { if(isGrouped) { @@ -550,23 +553,23 @@ function computeLegendDimensions(gd, groups, traces) { Drawing.setTranslate(this, borderwidth, - (5 + borderwidth + opts.height + textHeight / 2)); + (5 + borderwidth + opts._height + textHeight / 2)); - opts.height += textHeight; - opts.width = Math.max(opts.width, textWidth); + opts._height += textHeight; + opts._width = Math.max(opts._width, textWidth); }); - opts.width += 45 + borderwidth * 2; - opts.height += 10 + borderwidth * 2; + opts._width += 45 + borderwidth * 2; + opts._height += 10 + borderwidth * 2; if(isGrouped) { - opts.height += (opts._lgroupsLength - 1) * opts.tracegroupgap; + opts._height += (opts._lgroupsLength - 1) * opts.tracegroupgap; } extraWidth = 40; } else if(isGrouped) { - var groupXOffsets = [opts.width], + var groupXOffsets = [opts._width], groupData = groups.data(); for(var i = 0, n = groupData.length; i < n; i++) { @@ -576,9 +579,9 @@ function computeLegendDimensions(gd, groups, traces) { var groupWidth = 40 + Math.max.apply(null, textWidths); - opts.width += opts.tracegroupgap + groupWidth; + opts._width += opts.tracegroupgap + groupWidth; - groupXOffsets.push(opts.width); + groupXOffsets.push(opts._width); } groups.each(function(d, i) { @@ -601,11 +604,11 @@ function computeLegendDimensions(gd, groups, traces) { groupHeight += textHeight; }); - opts.height = Math.max(opts.height, groupHeight); + opts._height = Math.max(opts._height, groupHeight); }); - opts.height += 10 + borderwidth * 2; - opts.width += borderwidth * 2; + opts._height += 10 + borderwidth * 2; + opts._width += borderwidth * 2; } else { var rowHeight = 0, @@ -631,7 +634,7 @@ function computeLegendDimensions(gd, groups, traces) { if((borderwidth + offsetX + traceGap + traceWidth) > (fullLayout.width - (fullLayout.margin.r + fullLayout.margin.l))) { offsetX = 0; rowHeight = rowHeight + maxTraceHeight; - opts.height = opts.height + maxTraceHeight; + opts._height = opts._height + maxTraceHeight; // reset for next row maxTraceHeight = 0; } @@ -640,22 +643,22 @@ function computeLegendDimensions(gd, groups, traces) { (borderwidth + offsetX), (5 + borderwidth + legendItem.height / 2) + rowHeight); - opts.width += traceGap + traceWidth; - opts.height = Math.max(opts.height, legendItem.height); + opts._width += traceGap + traceWidth; + opts._height = Math.max(opts._height, legendItem.height); // keep track of tallest trace in group offsetX += traceGap + traceWidth; maxTraceHeight = Math.max(legendItem.height, maxTraceHeight); }); - opts.width += borderwidth * 2; - opts.height += 10 + borderwidth * 2; + opts._width += borderwidth * 2; + opts._height += 10 + borderwidth * 2; } // make sure we're only getting full pixels - opts.width = Math.ceil(opts.width); - opts.height = Math.ceil(opts.height); + opts._width = Math.ceil(opts._width); + opts._height = Math.ceil(opts._height); traces.each(function(d) { var legendItem = d[0], @@ -664,7 +667,7 @@ function computeLegendDimensions(gd, groups, traces) { bg.call(Drawing.setRect, 0, -legendItem.height / 2, - (gd._context.edits.legendText ? 0 : opts.width) + extraWidth, + (gd._context.edits.legendText ? 0 : opts._width) + extraWidth, legendItem.height ); }); @@ -694,10 +697,10 @@ function expandMargin(gd) { Plots.autoMargin(gd, 'legend', { x: opts.x, y: opts.y, - l: opts.width * ({right: 1, center: 0.5}[xanchor] || 0), - r: opts.width * ({left: 1, center: 0.5}[xanchor] || 0), - b: opts.height * ({top: 1, middle: 0.5}[yanchor] || 0), - t: opts.height * ({bottom: 1, middle: 0.5}[yanchor] || 0) + l: opts._width * (FROM_TL[xanchor]), + r: opts._width * (FROM_BR[xanchor]), + b: opts._height * (FROM_BR[yanchor]), + t: opts._height * (FROM_TL[yanchor]) }); } @@ -717,8 +720,8 @@ function expandHorizontalMargin(gd) { Plots.autoMargin(gd, 'legend', { x: opts.x, y: 0.5, - l: opts.width * ({right: 1, center: 0.5}[xanchor] || 0), - r: opts.width * ({left: 1, center: 0.5}[xanchor] || 0), + l: opts._width * (FROM_TL[xanchor]), + r: opts._width * (FROM_BR[xanchor]), b: 0, t: 0 }); diff --git a/src/components/rangeselector/draw.js b/src/components/rangeselector/draw.js index a2631065fd2..c5730804d24 100644 --- a/src/components/rangeselector/draw.js +++ b/src/components/rangeselector/draw.js @@ -19,7 +19,10 @@ var svgTextUtils = require('../../lib/svg_text_utils'); var axisIds = require('../../plots/cartesian/axis_ids'); var anchorUtils = require('../legend/anchor_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 constants = require('./constants'); var getUpdateObject = require('./get_update_object'); @@ -58,7 +61,7 @@ module.exports = function draw(gd) { var button = d3.select(this); var update = getUpdateObject(axisLayout, d); - d.isActive = isActive(axisLayout, d, update); + d._isActive = isActive(axisLayout, d, update); button.call(drawButtonRect, selectorLayout, d); button.call(drawButtonText, selectorLayout, d, gd); @@ -70,22 +73,17 @@ module.exports = function draw(gd) { }); button.on('mouseover', function() { - d.isHovered = true; + d._isHovered = true; button.call(drawButtonRect, selectorLayout, d); }); button.on('mouseout', function() { - d.isHovered = false; + d._isHovered = false; button.call(drawButtonRect, selectorLayout, d); }); }); - // N.B. this mutates selectorLayout - reposition(gd, buttons, selectorLayout, axisLayout._name); - - selector.attr('transform', 'translate(' + - selectorLayout.lx + ',' + selectorLayout.ly + - ')'); + reposition(gd, buttons, selectorLayout, axisLayout._name, selector); }); }; @@ -143,7 +141,7 @@ function drawButtonRect(button, selectorLayout, d) { } function getFillColor(selectorLayout, d) { - return (d.isActive || d.isHovered) ? + return (d._isActive || d._isHovered) ? selectorLayout.activecolor : selectorLayout.bgcolor; } @@ -175,9 +173,9 @@ function getLabel(opts) { return opts.count + opts.step.charAt(0); } -function reposition(gd, buttons, opts, axName) { - opts.width = 0; - opts.height = 0; +function reposition(gd, buttons, opts, axName, selector) { + var width = 0; + var height = 0; var borderWidth = opts.borderwidth; @@ -188,7 +186,7 @@ function reposition(gd, buttons, opts, axName) { var tHeight = opts.font.size * LINE_SPACING; var hEff = Math.max(tHeight * svgTextUtils.lineCount(text), 16) + 3; - opts.height = Math.max(opts.height, hEff); + height = Math.max(height, hEff); }); buttons.each(function() { @@ -207,59 +205,59 @@ function reposition(gd, buttons, opts, axName) { // TODO add buttongap attribute button.attr('transform', 'translate(' + - (borderWidth + opts.width) + ',' + borderWidth + + (borderWidth + width) + ',' + borderWidth + ')'); rect.attr({ x: 0, y: 0, width: wEff, - height: opts.height + height: height }); svgTextUtils.positionText(text, wEff / 2, - opts.height / 2 - ((tLines - 1) * tHeight / 2) + 3); + height / 2 - ((tLines - 1) * tHeight / 2) + 3); - opts.width += wEff + 5; + width += wEff + 5; }); - buttons.selectAll('rect').attr('height', opts.height); - var graphSize = gd._fullLayout._size; - opts.lx = graphSize.l + graphSize.w * opts.x; - opts.ly = graphSize.t + graphSize.h * (1 - opts.y); + var lx = graphSize.l + graphSize.w * opts.x; + var ly = graphSize.t + graphSize.h * (1 - opts.y); var xanchor = 'left'; if(anchorUtils.isRightAnchor(opts)) { - opts.lx -= opts.width; + lx -= width; xanchor = 'right'; } if(anchorUtils.isCenterAnchor(opts)) { - opts.lx -= opts.width / 2; + lx -= width / 2; xanchor = 'center'; } var yanchor = 'top'; if(anchorUtils.isBottomAnchor(opts)) { - opts.ly -= opts.height; + ly -= height; yanchor = 'bottom'; } if(anchorUtils.isMiddleAnchor(opts)) { - opts.ly -= opts.height / 2; + ly -= height / 2; yanchor = 'middle'; } - opts.width = Math.ceil(opts.width); - opts.height = Math.ceil(opts.height); - opts.lx = Math.round(opts.lx); - opts.ly = Math.round(opts.ly); + width = Math.ceil(width); + height = Math.ceil(height); + lx = Math.round(lx); + ly = Math.round(ly); Plots.autoMargin(gd, axName + '-range-selector', { x: opts.x, y: opts.y, - l: opts.width * ({right: 1, center: 0.5}[xanchor] || 0), - r: opts.width * ({left: 1, center: 0.5}[xanchor] || 0), - b: opts.height * ({top: 1, middle: 0.5}[yanchor] || 0), - t: opts.height * ({bottom: 1, middle: 0.5}[yanchor] || 0) + l: width * FROM_TL[xanchor], + r: width * FROM_BR[xanchor], + b: height * FROM_BR[yanchor], + t: height * FROM_TL[yanchor] }); + + selector.attr('transform', 'translate(' + lx + ',' + ly + ')'); } diff --git a/src/components/rangeslider/attributes.js b/src/components/rangeslider/attributes.js index 9b058be50da..37268dff026 100644 --- a/src/components/rangeslider/attributes.js +++ b/src/components/rangeslider/attributes.js @@ -38,6 +38,7 @@ module.exports = { dflt: true, role: 'style', editType: 'calc', + impliedEdits: {'range[0]': undefined, 'range[1]': undefined}, description: [ 'Determines whether or not the range slider range is', 'computed in relation to the input data.', @@ -48,10 +49,11 @@ module.exports = { valType: 'info_array', role: 'info', items: [ - {valType: 'any', editType: 'calc'}, - {valType: 'any', editType: 'calc'} + {valType: 'any', editType: 'calc', impliedEdits: {'^autorange': false}}, + {valType: 'any', editType: 'calc', impliedEdits: {'^autorange': false}} ], editType: 'calc', + impliedEdits: {'autorange': false}, description: [ 'Sets the range of the range slider.', 'If not set, defaults to the full xaxis range.', diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js index d4c7ce6acc2..106934833fa 100644 --- a/src/components/rangeslider/defaults.js +++ b/src/components/rangeslider/defaults.js @@ -38,18 +38,6 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) { coerce('autorange', !axOut.isValidRange(containerIn.range)); coerce('range'); - // Expand slider range to the axis range - // TODO: what if the ranges are reversed? - if(containerOut.range) { - var outRange = containerOut.range, - axRange = axOut.range; - - outRange[0] = axOut.l2r(Math.min(axOut.r2l(outRange[0]), axOut.r2l(axRange[0]))); - outRange[1] = axOut.l2r(Math.max(axOut.r2l(outRange[1]), axOut.r2l(axRange[1]))); - } - - axOut.cleanRange('rangeslider.range'); - // to map back range slider (auto) range containerOut._input = containerIn; }; diff --git a/src/components/rangeslider/draw.js b/src/components/rangeslider/draw.js index 11c5ec6431c..2690438d9b1 100644 --- a/src/components/rangeslider/draw.js +++ b/src/components/rangeslider/draw.js @@ -80,6 +80,21 @@ module.exports = function(gd) { opts = axisOpts[constants.name], oppAxisOpts = fullLayout[Axes.id2name(axisOpts.anchor)]; + // update range + // Expand slider range to the axis range + // TODO: what if the ranges are reversed? + if(opts.range) { + var outRange = opts.range; + var axRange = axisOpts.range; + + outRange[0] = axisOpts.l2r(Math.min(axisOpts.r2l(outRange[0]), axisOpts.r2l(axRange[0]))); + outRange[1] = axisOpts.l2r(Math.max(axisOpts.r2l(outRange[1]), axisOpts.r2l(axRange[1]))); + opts._input.range = outRange.slice(); + } + + axisOpts.cleanRange('rangeslider.range'); + + // update range slider dimensions var margin = fullLayout.margin, diff --git a/src/components/shapes/attributes.js b/src/components/shapes/attributes.js index 0a65317d58f..622cf147015 100644 --- a/src/components/shapes/attributes.js +++ b/src/components/shapes/attributes.js @@ -20,7 +20,7 @@ module.exports = { valType: 'boolean', role: 'info', dflt: true, - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', description: [ 'Determines whether or not this shape is visible.' ].join(' ') @@ -30,7 +30,7 @@ module.exports = { valType: 'enumerated', values: ['circle', 'rect', 'path', 'line'], role: 'info', - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', description: [ 'Specifies the shape type to be drawn.', @@ -74,7 +74,7 @@ module.exports = { x0: { valType: 'any', role: 'info', - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', description: [ 'Sets the shape\'s starting x position.', 'See `type` for more info.' @@ -83,7 +83,7 @@ module.exports = { x1: { valType: 'any', role: 'info', - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', description: [ 'Sets the shape\'s end x position.', 'See `type` for more info.' @@ -103,7 +103,7 @@ module.exports = { y0: { valType: 'any', role: 'info', - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', description: [ 'Sets the shape\'s starting y position.', 'See `type` for more info.' @@ -112,7 +112,7 @@ module.exports = { y1: { valType: 'any', role: 'info', - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', description: [ 'Sets the shape\'s end y position.', 'See `type` for more info.' @@ -122,7 +122,7 @@ module.exports = { path: { valType: 'string', role: 'info', - editType: 'calcIfAutorange', + editType: 'calcIfAutorange+arraydraw', description: [ 'For `type` *path* - a valid SVG path but with the pixel values', 'replaced by data values. There are a few restrictions / quirks', @@ -158,10 +158,10 @@ module.exports = { }, line: { color: extendFlat({}, scatterLineAttrs.color, {editType: 'arraydraw'}), - width: extendFlat({}, scatterLineAttrs.width, {editType: 'calcIfAutorange'}), + width: extendFlat({}, scatterLineAttrs.width, {editType: 'calcIfAutorange+arraydraw'}), dash: extendFlat({}, dash, {editType: 'arraydraw'}), role: 'info', - editType: 'calcIfAutorange' + editType: 'calcIfAutorange+arraydraw' }, fillcolor: { valType: 'color', diff --git a/src/components/sliders/draw.js b/src/components/sliders/draw.js index bd4941f7e96..1e7dff5caa4 100644 --- a/src/components/sliders/draw.js +++ b/src/components/sliders/draw.js @@ -18,7 +18,10 @@ var svgTextUtils = require('../../lib/svg_text_utils'); var anchorUtils = require('../legend/anchor_utils'); var constants = require('./constants'); -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; module.exports = function draw(gd) { @@ -98,7 +101,7 @@ function makeSliderData(fullLayout, gd) { for(var i = 0; i < contOpts.length; i++) { var item = contOpts[i]; if(!item.visible || !item.steps.length) continue; - item.gd = gd; + item._gd = gd; sliderData.push(item); } @@ -136,7 +139,9 @@ function findDimensions(gd, sliderOpts) { sliderLabels.remove(); - sliderOpts.inputAreaWidth = Math.max( + var dims = sliderOpts._dims = {}; + + dims.inputAreaWidth = Math.max( constants.railWidth, constants.gripHeight ); @@ -144,37 +149,33 @@ function findDimensions(gd, sliderOpts) { // calculate some overall dimensions - some of these are needed for // calculating the currentValue dimensions var graphSize = gd._fullLayout._size; - sliderOpts.lx = graphSize.l + graphSize.w * sliderOpts.x; - sliderOpts.ly = graphSize.t + graphSize.h * (1 - sliderOpts.y); + dims.lx = graphSize.l + graphSize.w * sliderOpts.x; + dims.ly = graphSize.t + graphSize.h * (1 - sliderOpts.y); if(sliderOpts.lenmode === 'fraction') { // fraction: - sliderOpts.outerLength = Math.round(graphSize.w * sliderOpts.len); + dims.outerLength = Math.round(graphSize.w * sliderOpts.len); } else { // pixels: - sliderOpts.outerLength = sliderOpts.len; + dims.outerLength = sliderOpts.len; } - // Set the length-wise padding so that the grip ends up *on* the end of - // the bar when at either extreme - sliderOpts.lenPad = Math.round(constants.gripWidth * 0.5); - // The length of the rail, *excluding* padding on either end: - sliderOpts.inputAreaStart = 0; - sliderOpts.inputAreaLength = Math.round(sliderOpts.outerLength - sliderOpts.pad.l - sliderOpts.pad.r); + dims.inputAreaStart = 0; + dims.inputAreaLength = Math.round(dims.outerLength - sliderOpts.pad.l - sliderOpts.pad.r); - var textableInputLength = sliderOpts.inputAreaLength - 2 * constants.stepInset; + var textableInputLength = dims.inputAreaLength - 2 * constants.stepInset; var availableSpacePerLabel = textableInputLength / (sliderOpts.steps.length - 1); var computedSpacePerLabel = maxLabelWidth + constants.labelPadding; - sliderOpts.labelStride = Math.max(1, Math.ceil(computedSpacePerLabel / availableSpacePerLabel)); - sliderOpts.labelHeight = labelHeight; + dims.labelStride = Math.max(1, Math.ceil(computedSpacePerLabel / availableSpacePerLabel)); + dims.labelHeight = labelHeight; // loop over all possible values for currentValue to find the // area we need for it - sliderOpts.currentValueMaxWidth = 0; - sliderOpts.currentValueHeight = 0; - sliderOpts.currentValueTotalHeight = 0; - sliderOpts.currentValueMaxLines = 1; + dims.currentValueMaxWidth = 0; + dims.currentValueHeight = 0; + dims.currentValueTotalHeight = 0; + dims.currentValueMaxLines = 1; if(sliderOpts.currentvalue.visible) { // Get the dimensions of the current value label: @@ -184,50 +185,50 @@ function findDimensions(gd, sliderOpts) { var curValPrefix = drawCurrentValue(dummyGroup, sliderOpts, stepOpts.label); var curValSize = (curValPrefix.node() && Drawing.bBox(curValPrefix.node())) || {width: 0, height: 0}; var lines = svgTextUtils.lineCount(curValPrefix); - sliderOpts.currentValueMaxWidth = Math.max(sliderOpts.currentValueMaxWidth, Math.ceil(curValSize.width)); - sliderOpts.currentValueHeight = Math.max(sliderOpts.currentValueHeight, Math.ceil(curValSize.height)); - sliderOpts.currentValueMaxLines = Math.max(sliderOpts.currentValueMaxLines, lines); + dims.currentValueMaxWidth = Math.max(dims.currentValueMaxWidth, Math.ceil(curValSize.width)); + dims.currentValueHeight = Math.max(dims.currentValueHeight, Math.ceil(curValSize.height)); + dims.currentValueMaxLines = Math.max(dims.currentValueMaxLines, lines); }); - sliderOpts.currentValueTotalHeight = sliderOpts.currentValueHeight + sliderOpts.currentvalue.offset; + dims.currentValueTotalHeight = dims.currentValueHeight + sliderOpts.currentvalue.offset; dummyGroup.remove(); } - sliderOpts.height = sliderOpts.currentValueTotalHeight + constants.tickOffset + sliderOpts.ticklen + constants.labelOffset + sliderOpts.labelHeight + sliderOpts.pad.t + sliderOpts.pad.b; + dims.height = dims.currentValueTotalHeight + constants.tickOffset + sliderOpts.ticklen + constants.labelOffset + dims.labelHeight + sliderOpts.pad.t + sliderOpts.pad.b; var xanchor = 'left'; if(anchorUtils.isRightAnchor(sliderOpts)) { - sliderOpts.lx -= sliderOpts.outerLength; + dims.lx -= dims.outerLength; xanchor = 'right'; } if(anchorUtils.isCenterAnchor(sliderOpts)) { - sliderOpts.lx -= sliderOpts.outerLength / 2; + dims.lx -= dims.outerLength / 2; xanchor = 'center'; } var yanchor = 'top'; if(anchorUtils.isBottomAnchor(sliderOpts)) { - sliderOpts.ly -= sliderOpts.height; + dims.ly -= dims.height; yanchor = 'bottom'; } if(anchorUtils.isMiddleAnchor(sliderOpts)) { - sliderOpts.ly -= sliderOpts.height / 2; + dims.ly -= dims.height / 2; yanchor = 'middle'; } - sliderOpts.outerLength = Math.ceil(sliderOpts.outerLength); - sliderOpts.height = Math.ceil(sliderOpts.height); - sliderOpts.lx = Math.round(sliderOpts.lx); - sliderOpts.ly = Math.round(sliderOpts.ly); + dims.outerLength = Math.ceil(dims.outerLength); + dims.height = Math.ceil(dims.height); + dims.lx = Math.round(dims.lx); + dims.ly = Math.round(dims.ly); Plots.autoMargin(gd, constants.autoMarginIdRoot + sliderOpts._index, { x: sliderOpts.x, y: sliderOpts.y, - l: sliderOpts.outerLength * ({right: 1, center: 0.5}[xanchor] || 0), - r: sliderOpts.outerLength * ({left: 1, center: 0.5}[xanchor] || 0), - b: sliderOpts.height * ({top: 1, middle: 0.5}[yanchor] || 0), - t: sliderOpts.height * ({bottom: 1, middle: 0.5}[yanchor] || 0) + l: dims.outerLength * FROM_TL[xanchor], + r: dims.outerLength * FROM_BR[xanchor], + b: dims.height * FROM_BR[yanchor], + t: dims.height * FROM_TL[yanchor] }); } @@ -250,8 +251,10 @@ function drawSlider(gd, sliderGroup, sliderOpts) { .call(drawTouchRect, gd, sliderOpts) .call(drawGrip, gd, sliderOpts); + var dims = sliderOpts._dims; + // Position the rectangle: - Drawing.setTranslate(sliderGroup, sliderOpts.lx + sliderOpts.pad.l, sliderOpts.ly + sliderOpts.pad.t); + Drawing.setTranslate(sliderGroup, dims.lx + sliderOpts.pad.l, dims.ly + sliderOpts.pad.t); sliderGroup.call(setGripPosition, sliderOpts, sliderOpts.active / (sliderOpts.steps.length - 1), false); sliderGroup.call(drawCurrentValue, sliderOpts); @@ -264,17 +267,18 @@ function drawCurrentValue(sliderGroup, sliderOpts, valueOverride) { var x0, textAnchor; var text = sliderGroup.selectAll('text') .data([0]); + var dims = sliderOpts._dims; switch(sliderOpts.currentvalue.xanchor) { case 'right': // This is anchored left and adjusted by the width of the longest label // so that the prefix doesn't move. The goal of this is to emphasize // what's actually changing and make the update less distracting. - x0 = sliderOpts.inputAreaLength - constants.currentValueInset - sliderOpts.currentValueMaxWidth; + x0 = dims.inputAreaLength - constants.currentValueInset - dims.currentValueMaxWidth; textAnchor = 'left'; break; case 'center': - x0 = sliderOpts.inputAreaLength * 0.5; + x0 = dims.inputAreaLength * 0.5; textAnchor = 'middle'; break; default: @@ -305,11 +309,11 @@ function drawCurrentValue(sliderGroup, sliderOpts, valueOverride) { text.call(Drawing.font, sliderOpts.currentvalue.font) .text(str) - .call(svgTextUtils.convertToTspans, sliderOpts.gd); + .call(svgTextUtils.convertToTspans, sliderOpts._gd); var lines = svgTextUtils.lineCount(text); - var y0 = (sliderOpts.currentValueMaxLines + 1 - lines) * + var y0 = (dims.currentValueMaxLines + 1 - lines) * sliderOpts.currentvalue.font.size * LINE_SPACING; svgTextUtils.positionText(text, x0, y0); @@ -351,7 +355,7 @@ function drawLabel(item, data, sliderOpts) { text.call(Drawing.font, sliderOpts.font) .text(data.step.label) - .call(svgTextUtils.convertToTspans, sliderOpts.gd); + .call(svgTextUtils.convertToTspans, sliderOpts._gd); return text; } @@ -359,12 +363,13 @@ function drawLabel(item, data, sliderOpts) { function drawLabelGroup(sliderGroup, sliderOpts) { var labels = sliderGroup.selectAll('g.' + constants.labelsClass) .data([0]); + var dims = sliderOpts._dims; labels.enter().append('g') .classed(constants.labelsClass, true); var labelItems = labels.selectAll('g.' + constants.labelGroupClass) - .data(sliderOpts.labelSteps); + .data(dims.labelSteps); labelItems.enter().append('g') .classed(constants.labelGroupClass, true); @@ -384,7 +389,7 @@ function drawLabelGroup(sliderGroup, sliderOpts) { // if the label spans multiple lines sliderOpts.font.size * LINE_SPACING + constants.labelOffset + - sliderOpts.currentValueTotalHeight + dims.currentValueTotalHeight ); }); @@ -488,6 +493,7 @@ function attachGripEvents(item, gd, sliderGroup) { function drawTicks(sliderGroup, sliderOpts) { var tick = sliderGroup.selectAll('rect.' + constants.tickRectClass) .data(sliderOpts.steps); + var dims = sliderOpts._dims; tick.enter().append('rect') .classed(constants.tickRectClass, true); @@ -500,7 +506,7 @@ function drawTicks(sliderGroup, sliderOpts) { }); tick.each(function(d, i) { - var isMajor = i % sliderOpts.labelStride === 0; + var isMajor = i % dims.labelStride === 0; var item = d3.select(this); item @@ -509,19 +515,20 @@ function drawTicks(sliderGroup, sliderOpts) { Drawing.setTranslate(item, normalizedValueToPosition(sliderOpts, i / (sliderOpts.steps.length - 1)) - 0.5 * sliderOpts.tickwidth, - (isMajor ? constants.tickOffset : constants.minorTickOffset) + sliderOpts.currentValueTotalHeight + (isMajor ? constants.tickOffset : constants.minorTickOffset) + dims.currentValueTotalHeight ); }); } function computeLabelSteps(sliderOpts) { - sliderOpts.labelSteps = []; + var dims = sliderOpts._dims; + dims.labelSteps = []; var i0 = 0; var nsteps = sliderOpts.steps.length; - for(var i = i0; i < nsteps; i += sliderOpts.labelStride) { - sliderOpts.labelSteps.push({ + for(var i = i0; i < nsteps; i += dims.labelStride) { + dims.labelSteps.push({ fraction: i / (nsteps - 1), step: sliderOpts.steps[i] }); @@ -546,23 +553,26 @@ function setGripPosition(sliderGroup, sliderOpts, position, doTransition) { // Drawing.setTranslate doesn't work here becasue of the transition duck-typing. // It's also not necessary because there are no other transitions to preserve. - el.attr('transform', 'translate(' + (x - constants.gripWidth * 0.5) + ',' + (sliderOpts.currentValueTotalHeight) + ')'); + el.attr('transform', 'translate(' + (x - constants.gripWidth * 0.5) + ',' + (sliderOpts._dims.currentValueTotalHeight) + ')'); } // Convert a number from [0-1] to a pixel position relative to the slider group container: function normalizedValueToPosition(sliderOpts, normalizedPosition) { - return sliderOpts.inputAreaStart + constants.stepInset + - (sliderOpts.inputAreaLength - 2 * constants.stepInset) * Math.min(1, Math.max(0, normalizedPosition)); + var dims = sliderOpts._dims; + return dims.inputAreaStart + constants.stepInset + + (dims.inputAreaLength - 2 * constants.stepInset) * Math.min(1, Math.max(0, normalizedPosition)); } // Convert a position relative to the slider group to a nubmer in [0, 1] function positionToNormalizedValue(sliderOpts, position) { - return Math.min(1, Math.max(0, (position - constants.stepInset - sliderOpts.inputAreaStart) / (sliderOpts.inputAreaLength - 2 * constants.stepInset - 2 * sliderOpts.inputAreaStart))); + var dims = sliderOpts._dims; + return Math.min(1, Math.max(0, (position - constants.stepInset - dims.inputAreaStart) / (dims.inputAreaLength - 2 * constants.stepInset - 2 * dims.inputAreaStart))); } function drawTouchRect(sliderGroup, gd, sliderOpts) { var rect = sliderGroup.selectAll('rect.' + constants.railTouchRectClass) .data([0]); + var dims = sliderOpts._dims; rect.enter().append('rect') .classed(constants.railTouchRectClass, true) @@ -570,23 +580,24 @@ function drawTouchRect(sliderGroup, gd, sliderOpts) { .style('pointer-events', 'all'); rect.attr({ - width: sliderOpts.inputAreaLength, - height: Math.max(sliderOpts.inputAreaWidth, constants.tickOffset + sliderOpts.ticklen + sliderOpts.labelHeight) + width: dims.inputAreaLength, + height: Math.max(dims.inputAreaWidth, constants.tickOffset + sliderOpts.ticklen + dims.labelHeight) }) .call(Color.fill, sliderOpts.bgcolor) .attr('opacity', 0); - Drawing.setTranslate(rect, 0, sliderOpts.currentValueTotalHeight); + Drawing.setTranslate(rect, 0, dims.currentValueTotalHeight); } function drawRail(sliderGroup, sliderOpts) { var rect = sliderGroup.selectAll('rect.' + constants.railRectClass) .data([0]); + var dims = sliderOpts._dims; rect.enter().append('rect') .classed(constants.railRectClass, true); - var computedLength = sliderOpts.inputAreaLength - constants.railInset * 2; + var computedLength = dims.inputAreaLength - constants.railInset * 2; rect.attr({ width: computedLength, @@ -601,7 +612,7 @@ function drawRail(sliderGroup, sliderOpts) { Drawing.setTranslate(rect, constants.railInset, - (sliderOpts.inputAreaWidth - constants.railWidth) * 0.5 + sliderOpts.currentValueTotalHeight + (dims.inputAreaWidth - constants.railWidth) * 0.5 + dims.currentValueTotalHeight ); } diff --git a/src/components/updatemenus/draw.js b/src/components/updatemenus/draw.js index 22fa00ade5b..ea916cef2f6 100644 --- a/src/components/updatemenus/draw.js +++ b/src/components/updatemenus/draw.js @@ -192,6 +192,7 @@ function setActive(gd, menuOpts, buttonOpts, gHeader, gButton, scrollBox, button function drawHeader(gd, gHeader, gButton, scrollBox, menuOpts) { var header = gHeader.selectAll('g.' + constants.headerClassName) .data([0]); + var dims = menuOpts._dims; header.enter().append('g') .classed(constants.headerClassName, true) @@ -201,8 +202,8 @@ function drawHeader(gd, gHeader, gButton, scrollBox, menuOpts) { headerOpts = menuOpts.buttons[active] || constants.blankHeaderOpts, posOpts = { y: menuOpts.pad.t, yPad: 0, x: menuOpts.pad.l, xPad: 0, index: 0 }, positionOverrides = { - width: menuOpts.headerWidth, - height: menuOpts.headerHeight + width: dims.headerWidth, + height: dims.headerHeight }; header @@ -221,8 +222,8 @@ function drawHeader(gd, gHeader, gButton, scrollBox, menuOpts) { .text(constants.arrowSymbol[menuOpts.direction]); arrow.attr({ - x: menuOpts.headerWidth - constants.arrowOffsetX + menuOpts.pad.l, - y: menuOpts.headerHeight / 2 + constants.textOffsetY + menuOpts.pad.t + x: dims.headerWidth - constants.arrowOffsetX + menuOpts.pad.l, + y: dims.headerHeight / 2 + constants.textOffsetY + menuOpts.pad.t }); header.on('click', function() { @@ -250,7 +251,7 @@ function drawHeader(gd, gHeader, gButton, scrollBox, menuOpts) { }); // translate header group - Drawing.setTranslate(gHeader, menuOpts.lx, menuOpts.ly); + Drawing.setTranslate(gHeader, dims.lx, dims.ly); } function drawButtons(gd, gHeader, gButton, scrollBox, menuOpts) { @@ -290,28 +291,29 @@ function drawButtons(gd, gHeader, gButton, scrollBox, menuOpts) { var x0 = 0; var y0 = 0; + var dims = menuOpts._dims; var isVertical = ['up', 'down'].indexOf(menuOpts.direction) !== -1; if(menuOpts.type === 'dropdown') { if(isVertical) { - y0 = menuOpts.headerHeight + constants.gapButtonHeader; + y0 = dims.headerHeight + constants.gapButtonHeader; } else { - x0 = menuOpts.headerWidth + constants.gapButtonHeader; + x0 = dims.headerWidth + constants.gapButtonHeader; } } if(menuOpts.type === 'dropdown' && menuOpts.direction === 'up') { - y0 = -constants.gapButtonHeader + constants.gapButton - menuOpts.openHeight; + y0 = -constants.gapButtonHeader + constants.gapButton - dims.openHeight; } if(menuOpts.type === 'dropdown' && menuOpts.direction === 'left') { - x0 = -constants.gapButtonHeader + constants.gapButton - menuOpts.openWidth; + x0 = -constants.gapButtonHeader + constants.gapButton - dims.openWidth; } var posOpts = { - x: menuOpts.lx + x0 + menuOpts.pad.l, - y: menuOpts.ly + y0 + menuOpts.pad.t, + x: dims.lx + x0 + menuOpts.pad.l, + y: dims.ly + y0 + menuOpts.pad.t, yPad: constants.gapButton, xPad: constants.gapButton, index: 0, @@ -355,12 +357,12 @@ function drawButtons(gd, gHeader, gButton, scrollBox, menuOpts) { buttons.call(styleButtons, menuOpts); if(isVertical) { - scrollBoxPosition.w = Math.max(menuOpts.openWidth, menuOpts.headerWidth); + scrollBoxPosition.w = Math.max(dims.openWidth, dims.headerWidth); scrollBoxPosition.h = posOpts.y - scrollBoxPosition.t; } else { scrollBoxPosition.w = posOpts.x - scrollBoxPosition.l; - scrollBoxPosition.h = Math.max(menuOpts.openHeight, menuOpts.headerHeight); + scrollBoxPosition.h = Math.max(dims.openHeight, dims.headerHeight); } scrollBoxPosition.direction = menuOpts.direction; @@ -377,8 +379,9 @@ function drawButtons(gd, gHeader, gButton, scrollBox, menuOpts) { function drawScrollBox(gd, gHeader, gButton, scrollBox, menuOpts, position) { // enable the scrollbox - var direction = menuOpts.direction, - isVertical = (direction === 'up' || direction === 'down'); + var direction = menuOpts.direction; + var isVertical = (direction === 'up' || direction === 'down'); + var dims = menuOpts._dims; var active = menuOpts.active, translateX, translateY, @@ -386,13 +389,13 @@ function drawScrollBox(gd, gHeader, gButton, scrollBox, menuOpts, position) { if(isVertical) { translateY = 0; for(i = 0; i < active; i++) { - translateY += menuOpts.heights[i] + constants.gapButton; + translateY += dims.heights[i] + constants.gapButton; } } else { translateX = 0; for(i = 0; i < active; i++) { - translateX += menuOpts.widths[i] + constants.gapButton; + translateX += dims.widths[i] + constants.gapButton; } } @@ -502,16 +505,18 @@ function styleOnMouseOut(item, menuOpts) { // find item dimensions (this mutates menuOpts) function findDimensions(gd, menuOpts) { - menuOpts.width1 = 0; - menuOpts.height1 = 0; - menuOpts.heights = []; - menuOpts.widths = []; - menuOpts.totalWidth = 0; - menuOpts.totalHeight = 0; - menuOpts.openWidth = 0; - menuOpts.openHeight = 0; - menuOpts.lx = 0; - menuOpts.ly = 0; + var dims = menuOpts._dims = { + width1: 0, + height1: 0, + heights: [], + widths: [], + totalWidth: 0, + totalHeight: 0, + openWidth: 0, + openHeight: 0, + lx: 0, + ly: 0 + }; var fakeButtons = Drawing.tester.selectAll('g.' + constants.dropdownButtonClassName) .data(menuOpts.buttons); @@ -543,79 +548,79 @@ function findDimensions(gd, menuOpts) { // Store per-item sizes since a row of horizontal buttons, for example, // don't all need to be the same width: - menuOpts.widths[i] = wEff; - menuOpts.heights[i] = hEff; + dims.widths[i] = wEff; + dims.heights[i] = hEff; // Height and width of individual element: - menuOpts.height1 = Math.max(menuOpts.height1, hEff); - menuOpts.width1 = Math.max(menuOpts.width1, wEff); + dims.height1 = Math.max(dims.height1, hEff); + dims.width1 = Math.max(dims.width1, wEff); if(isVertical) { - menuOpts.totalWidth = Math.max(menuOpts.totalWidth, wEff); - menuOpts.openWidth = menuOpts.totalWidth; - menuOpts.totalHeight += hEff + constants.gapButton; - menuOpts.openHeight += hEff + constants.gapButton; + dims.totalWidth = Math.max(dims.totalWidth, wEff); + dims.openWidth = dims.totalWidth; + dims.totalHeight += hEff + constants.gapButton; + dims.openHeight += hEff + constants.gapButton; } else { - menuOpts.totalWidth += wEff + constants.gapButton; - menuOpts.openWidth += wEff + constants.gapButton; - menuOpts.totalHeight = Math.max(menuOpts.totalHeight, hEff); - menuOpts.openHeight = menuOpts.totalHeight; + dims.totalWidth += wEff + constants.gapButton; + dims.openWidth += wEff + constants.gapButton; + dims.totalHeight = Math.max(dims.totalHeight, hEff); + dims.openHeight = dims.totalHeight; } }); if(isVertical) { - menuOpts.totalHeight -= constants.gapButton; + dims.totalHeight -= constants.gapButton; } else { - menuOpts.totalWidth -= constants.gapButton; + dims.totalWidth -= constants.gapButton; } - menuOpts.headerWidth = menuOpts.width1 + constants.arrowPadX; - menuOpts.headerHeight = menuOpts.height1; + dims.headerWidth = dims.width1 + constants.arrowPadX; + dims.headerHeight = dims.height1; if(menuOpts.type === 'dropdown') { if(isVertical) { - menuOpts.width1 += constants.arrowPadX; - menuOpts.totalHeight = menuOpts.height1; + dims.width1 += constants.arrowPadX; + dims.totalHeight = dims.height1; } else { - menuOpts.totalWidth = menuOpts.width1; + dims.totalWidth = dims.width1; } - menuOpts.totalWidth += constants.arrowPadX; + dims.totalWidth += constants.arrowPadX; } fakeButtons.remove(); - var paddedWidth = menuOpts.totalWidth + menuOpts.pad.l + menuOpts.pad.r; - var paddedHeight = menuOpts.totalHeight + menuOpts.pad.t + menuOpts.pad.b; + var paddedWidth = dims.totalWidth + menuOpts.pad.l + menuOpts.pad.r; + var paddedHeight = dims.totalHeight + menuOpts.pad.t + menuOpts.pad.b; var graphSize = gd._fullLayout._size; - menuOpts.lx = graphSize.l + graphSize.w * menuOpts.x; - menuOpts.ly = graphSize.t + graphSize.h * (1 - menuOpts.y); + dims.lx = graphSize.l + graphSize.w * menuOpts.x; + dims.ly = graphSize.t + graphSize.h * (1 - menuOpts.y); var xanchor = 'left'; if(anchorUtils.isRightAnchor(menuOpts)) { - menuOpts.lx -= paddedWidth; + dims.lx -= paddedWidth; xanchor = 'right'; } if(anchorUtils.isCenterAnchor(menuOpts)) { - menuOpts.lx -= paddedWidth / 2; + dims.lx -= paddedWidth / 2; xanchor = 'center'; } var yanchor = 'top'; if(anchorUtils.isBottomAnchor(menuOpts)) { - menuOpts.ly -= paddedHeight; + dims.ly -= paddedHeight; yanchor = 'bottom'; } if(anchorUtils.isMiddleAnchor(menuOpts)) { - menuOpts.ly -= paddedHeight / 2; + dims.ly -= paddedHeight / 2; yanchor = 'middle'; } - menuOpts.totalWidth = Math.ceil(menuOpts.totalWidth); - menuOpts.totalHeight = Math.ceil(menuOpts.totalHeight); - menuOpts.lx = Math.round(menuOpts.lx); - menuOpts.ly = Math.round(menuOpts.ly); + dims.totalWidth = Math.ceil(dims.totalWidth); + dims.totalHeight = Math.ceil(dims.totalHeight); + dims.lx = Math.round(dims.lx); + dims.ly = Math.round(dims.ly); Plots.autoMargin(gd, constants.autoMarginIdRoot + menuOpts._index, { x: menuOpts.x, @@ -634,16 +639,17 @@ function setItemPosition(item, menuOpts, posOpts, overrideOpts) { var text = item.select('.' + constants.itemTextClassName); var borderWidth = menuOpts.borderwidth; var index = posOpts.index; + var dims = menuOpts._dims; Drawing.setTranslate(item, borderWidth + posOpts.x, borderWidth + posOpts.y); var isVertical = ['up', 'down'].indexOf(menuOpts.direction) !== -1; - var finalHeight = overrideOpts.height || (isVertical ? menuOpts.heights[index] : menuOpts.height1); + var finalHeight = overrideOpts.height || (isVertical ? dims.heights[index] : dims.height1); rect.attr({ x: 0, y: 0, - width: overrideOpts.width || (isVertical ? menuOpts.width1 : menuOpts.widths[index]), + width: overrideOpts.width || (isVertical ? dims.width1 : dims.widths[index]), height: finalHeight }); @@ -655,9 +661,9 @@ function setItemPosition(item, menuOpts, posOpts, overrideOpts) { finalHeight / 2 - spanOffset + constants.textOffsetY); if(isVertical) { - posOpts.y += menuOpts.heights[index] + posOpts.yPad; + posOpts.y += dims.heights[index] + posOpts.yPad; } else { - posOpts.x += menuOpts.widths[index] + posOpts.xPad; + posOpts.x += dims.widths[index] + posOpts.xPad; } posOpts.index++; diff --git a/src/constants/alignment.js b/src/constants/alignment.js index 5e4d6836b38..a63cc18266d 100644 --- a/src/constants/alignment.js +++ b/src/constants/alignment.js @@ -29,6 +29,15 @@ module.exports = { middle: 0.5, top: 0 }, + // from bottom right: sometimes you just need the opposite of ^^ + FROM_BR: { + left: 1, + center: 0.5, + right: 0, + bottom: 0, + middle: 0.5, + top: 1 + }, // multiple of fontSize to get the vertical offset between lines LINE_SPACING: 1.3, diff --git a/src/core.js b/src/core.js index f8abab28eb4..8afc64171ab 100644 --- a/src/core.js +++ b/src/core.js @@ -33,6 +33,7 @@ exports.restyle = Plotly.restyle; exports.relayout = Plotly.relayout; exports.redraw = Plotly.redraw; exports.update = Plotly.update; +exports.react = Plotly.react; exports.extendTraces = Plotly.extendTraces; exports.prependTraces = Plotly.prependTraces; exports.addTraces = Plotly.addTraces; diff --git a/src/lib/coerce.js b/src/lib/coerce.js index 07627121c1f..26f15e57932 100644 --- a/src/lib/coerce.js +++ b/src/lib/coerce.js @@ -406,6 +406,9 @@ exports.coerceSelectionMarkerOpacity = function(traceOut, coerce) { if(!traceOut.marker) return; var mo = traceOut.marker.opacity; + // you can still have a `marker` container with no markers if there's text + if(mo === undefined) return; + var smoDflt; var usmoDflt; diff --git a/src/lib/push_unique.js b/src/lib/push_unique.js index 87b9f4cc672..ca2dcf30d4c 100644 --- a/src/lib/push_unique.js +++ b/src/lib/push_unique.js @@ -11,6 +11,8 @@ /** * Push array with unique items * + * Ignores falsy items, except 0 so we can use it to construct arrays of indices. + * * @param {array} array * array to be filled * @param {any} item @@ -30,7 +32,7 @@ module.exports = function pushUnique(array, item) { } array.push(item); } - else if(item && array.indexOf(item) === -1) array.push(item); + else if((item || item === 0) && array.indexOf(item) === -1) array.push(item); return array; }; diff --git a/src/plot_api/helpers.js b/src/plot_api/helpers.js index 5f0f5c20772..38a5d715dd5 100644 --- a/src/plot_api/helpers.js +++ b/src/plot_api/helpers.js @@ -196,8 +196,14 @@ function cleanAxRef(container, attr) { } } -// Make a few changes to the data right away -// before it gets used for anything +/* + * 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. + * + * 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 @@ -283,7 +289,9 @@ exports.cleanData = function(data, existingData) { if(!Registry.traceIs(trace, 'pie') && !Registry.traceIs(trace, 'bar')) { if(Array.isArray(trace.textposition)) { - trace.textposition = trace.textposition.map(cleanTextPosition); + for(i = 0; i < trace.textposition.length; i++) { + trace.textposition[i] = cleanTextPosition(trace.textposition[i]); + } } else if(trace.textposition) { trace.textposition = cleanTextPosition(trace.textposition); diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 644251611c4..9aa3f9712ce 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -58,6 +58,13 @@ var numericNameWarningCountLimit = 5; * @param {object} config * configuration options (see ./plot_config.js for more info) * + * OR + * + * @param {string id or DOM element} gd + * the id or DOM element of the graph container div + * @param {object} figure + * object containing `data`, `layout`, `config`, and `frames` members + * */ Plotly.plot = function(gd, data, layout, config) { var frames; @@ -1379,8 +1386,7 @@ function _restyle(gd, aobj, traces) { // for the undo / redo queue var redoit = {}, undoit = {}, - axlist, - flagAxForDelete = {}; + axlist; // make a new empty vals array for undoit function a0() { return traces.map(function() { return undefined; }); } @@ -1523,9 +1529,6 @@ function _restyle(gd, aobj, traces) { } else if(Registry.traceIs(cont, 'cartesian')) { Lib.nestedProperty(cont, 'marker.colors') .set(Lib.nestedProperty(cont, 'marker.color').get()); - // look for axes that are no longer in use and delete them - flagAxForDelete[cont.xaxis || 'x'] = true; - flagAxForDelete[cont.yaxis || 'y'] = true; } } @@ -1632,26 +1635,6 @@ function _restyle(gd, aobj, traces) { } } - // check axes we've flagged for possible deletion - // flagAxForDelete is a hash so we can make sure we only get each axis once - var axListForDelete = Object.keys(flagAxForDelete); - axisLoop: - for(i = 0; i < axListForDelete.length; i++) { - var axId = axListForDelete[i], - axLetter = axId.charAt(0), - axAttr = axLetter + 'axis'; - - for(var j = 0; j < data.length; j++) { - if(Registry.traceIs(data[j], 'cartesian') && - (data[j][axAttr] || axLetter) === axId) { - continue axisLoop; - } - } - - // no data on this axis - delete it. - doextra('LAYOUT' + Plotly.Axes.id2name(axId), null, 0); - } - // combine a few flags together; if(flags.calc || (flags.calcIfAutorange && autorangeOn)) { flags.clearCalc = true; @@ -1806,25 +1789,6 @@ function _relayout(gd, aobj) { if(val !== undefined) p.set(val); } - // for editing annotations or shapes - is it on autoscaled axes? - function refAutorange(obj, axLetter) { - if(!Lib.isPlainObject(obj)) return false; - var axRef = obj[axLetter + 'ref'] || axLetter, - ax = Plotly.Axes.getFromId(gd, axRef); - - if(!ax && axRef.charAt(0) === axLetter) { - // fall back on the primary axis in case we've referenced a - // nonexistent axis (as we do above if axRef is missing). - // This assumes the object defaults to data referenced, which - // is the case for shapes and annotations but not for images. - // The only thing this is used for is to determine whether to - // do a full `recalc`, so the only ill effect of this error is - // to waste some time. - ax = Plotly.Axes.getFromId(gd, axLetter); - } - return (ax || {}).autorange; - } - // for constraint enforcement: keep track of all axes (as {id: name}) // we're editing the (auto)range of, so we can tell the others constrained // to scale with them that it's OK for them to shrink @@ -2018,7 +1982,7 @@ function _relayout(gd, aobj) { else Lib.warn('unrecognized full object value', aobj); } - if(checkForAutorange && (refAutorange(objToAutorange, 'x') || refAutorange(objToAutorange, 'y'))) { + if(checkForAutorange && (refAutorange(gd, objToAutorange, 'x') || refAutorange(gd, objToAutorange, 'y'))) { flags.calc = true; } else editTypes.update(flags, updateValObject); @@ -2114,6 +2078,25 @@ function _relayout(gd, aobj) { }; } +// for editing annotations or shapes - is it on autoscaled axes? +function refAutorange(gd, obj, axLetter) { + if(!Lib.isPlainObject(obj)) return false; + var axRef = obj[axLetter + 'ref'] || axLetter, + ax = Plotly.Axes.getFromId(gd, axRef); + + if(!ax && axRef.charAt(0) === axLetter) { + // fall back on the primary axis in case we've referenced a + // nonexistent axis (as we do above if axRef is missing). + // This assumes the object defaults to data referenced, which + // is the case for shapes and annotations but not for images. + // The only thing this is used for is to determine whether to + // do a full `recalc`, so the only ill effect of this error is + // to waste some time. + ax = Plotly.Axes.getFromId(gd, axLetter); + } + return (ax || {}).autorange; +} + /** * update: update trace and layout attributes of an existing plot * @@ -2208,6 +2191,404 @@ Plotly.update = function update(gd, traceUpdate, layoutUpdate, _traces) { }); }; +/** + * Plotly.react: + * A plot/update method that takes the full plot state (same API as plot/newPlot) + * and diffs to determine the minimal update pathway + * + * @param {string id or DOM element} gd + * the id or DOM element of the graph container div + * @param {array of objects} data + * array of traces, containing the data and display information for each trace + * @param {object} layout + * object describing the overall display of the plot, + * all the stuff that doesn't pertain to any individual trace + * @param {object} config + * configuration options (see ./plot_config.js for more info) + * + * OR + * + * @param {string id or DOM element} gd + * the id or DOM element of the graph container div + * @param {object} figure + * object containing `data`, `layout`, `config`, and `frames` members + * + */ +Plotly.react = function(gd, data, layout, config) { + var frames, plotDone; + + function addFrames() { return Plotly.addFrames(gd, frames); } + + gd = Lib.getGraphDiv(gd); + + var oldFullData = gd._fullData; + var oldFullLayout = gd._fullLayout; + + // you can use this as the initial draw as well as to update + if(!Lib.isPlotDiv(gd) || !oldFullData || !oldFullLayout) { + plotDone = Plotly.newPlot(gd, data, layout, config); + } + else { + + if(Lib.isPlainObject(data)) { + var obj = data; + data = obj.data; + layout = obj.layout; + config = obj.config; + frames = obj.frames; + } + + var configChanged = false; + // assume that if there's a config at all, we're reacting to it too, + // and completely replace the previous config + if(config) { + var oldConfig = Lib.extendDeep({}, gd._context); + gd._context = undefined; + setPlotContext(gd, config); + configChanged = diffConfig(oldConfig, gd._context); + } + + gd.data = data || []; + helpers.cleanData(gd.data, []); + gd.layout = layout || {}; + helpers.cleanLayout(gd.layout); + + Plots.supplyDefaults(gd); + + var newFullData = gd._fullData; + var newFullLayout = gd._fullLayout; + var immutable = newFullLayout.datarevision === undefined; + + var restyleFlags = diffData(gd, oldFullData, newFullData, immutable); + var relayoutFlags = diffLayout(gd, oldFullLayout, newFullLayout, immutable); + + // clear calcdata if required + if(restyleFlags.calc || relayoutFlags.calc) gd.calcdata = undefined; + + // Note: what restyle/relayout use impliedEdits and clearAxisTypes for + // must be handled by the user when using Plotly.react. + + // fill in redraw sequence + var seq = []; + + if(frames) { + gd._transitionData = {}; + Plots.createTransitionData(gd); + seq.push(addFrames); + } + + if(restyleFlags.fullReplot || relayoutFlags.layoutReplot || configChanged) { + gd._fullLayout._skipDefaults = true; + seq.push(Plotly.plot); + } + else { + for(var componentType in relayoutFlags.arrays) { + var indices = relayoutFlags.arrays[componentType]; + if(indices.length) { + var drawOne = Registry.getComponentMethod(componentType, 'drawOne'); + if(drawOne !== Lib.noop) { + for(var i = 0; i < indices.length; i++) { + drawOne(gd, indices[i]); + } + } + else { + var draw = Registry.getComponentMethod(componentType, 'draw'); + if(draw === Lib.noop) { + throw new Error('cannot draw components: ' + componentType); + } + draw(gd); + } + } + } + + seq.push(Plots.previousPromises); + if(restyleFlags.style) seq.push(subroutines.doTraceStyle); + if(restyleFlags.colorbars) seq.push(subroutines.doColorBars); + if(relayoutFlags.legend) seq.push(subroutines.doLegend); + if(relayoutFlags.layoutstyle) seq.push(subroutines.layoutStyles); + if(relayoutFlags.ticks) seq.push(subroutines.doTicksRelayout); + if(relayoutFlags.modebar) seq.push(subroutines.doModeBar); + if(relayoutFlags.camera) seq.push(subroutines.doCamera); + } + + seq.push(Plots.rehover); + + plotDone = Lib.syncOrAsync(seq, gd); + if(!plotDone || !plotDone.then) plotDone = Promise.resolve(gd); + } + + return plotDone.then(function() { + gd.emit('plotly_react', { + data: data, + layout: layout + }); + + return gd; + }); + +}; + +function diffData(gd, oldFullData, newFullData, immutable) { + if(oldFullData.length !== newFullData.length) { + return { + fullReplot: true, + clearCalc: true + }; + } + + var flags = editTypes.traceFlags(); + flags.arrays = {}; + var i, trace; + + function getTraceValObject(parts) { + return PlotSchema.getTraceValObject(trace, parts); + } + + var diffOpts = { + getValObject: getTraceValObject, + flags: flags, + immutable: immutable, + gd: gd + }; + + for(i = 0; i < oldFullData.length; i++) { + trace = newFullData[i]; + diffOpts.autoranged = trace.xaxis ? ( + Plotly.Axes.getFromId(gd, trace.xaxis).autorange || + Plotly.Axes.getFromId(gd, trace.yaxis).autorange + ) : false; + getDiffFlags(oldFullData[i], trace, [], diffOpts); + } + + if(flags.calc || flags.plot || flags.calcIfAutorange) { + flags.fullReplot = true; + } + + return flags; +} + +function diffLayout(gd, oldFullLayout, newFullLayout, immutable) { + var flags = editTypes.layoutFlags(); + flags.arrays = {}; + + function getLayoutValObject(parts) { + return PlotSchema.getLayoutValObject(newFullLayout, parts); + } + + var diffOpts = { + getValObject: getLayoutValObject, + flags: flags, + immutable: immutable, + gd: gd + }; + + getDiffFlags(oldFullLayout, newFullLayout, [], diffOpts); + + if(flags.plot || flags.calc) { + flags.layoutReplot = true; + } + + return flags; +} + +function getDiffFlags(oldContainer, newContainer, outerparts, opts) { + var valObject, key; + + var getValObject = opts.getValObject; + var flags = opts.flags; + var immutable = opts.immutable; + var inArray = opts.inArray; + var arrayIndex = opts.arrayIndex; + var gd = opts.gd; + var autoranged = opts.autoranged; + + function changed() { + var editType = valObject.editType; + if(editType.indexOf('calcIfAutorange') !== -1 && (autoranged || (autoranged === undefined && ( + refAutorange(gd, newContainer, 'x') || refAutorange(gd, newContainer, 'y') + )))) { + flags.calc = true; + return; + } + if(inArray && editType.indexOf('arraydraw') !== -1) { + Lib.pushUnique(flags.arrays[inArray], arrayIndex); + return; + } + editTypes.update(flags, valObject); + } + + function valObjectCanBeDataArray(valObject) { + return valObject.valType === 'data_array' || valObject.arrayOk; + } + + // for transforms: look at _fullInput rather than the transform result, which often + // contains generated arrays. + var newFullInput = newContainer._fullInput; + var oldFullInput = oldContainer._fullInput; + if(newFullInput && newFullInput !== newContainer) newContainer = newFullInput; + if(oldFullInput && oldFullInput !== oldContainer) oldContainer = oldFullInput; + + for(key in oldContainer) { + // short-circuit based on previous calls or previous keys that already maximized the pathway + if(flags.calc) return; + + var oldVal = oldContainer[key]; + var newVal = newContainer[key]; + + if(key.charAt(0) === '_' || typeof oldVal === 'function' || oldVal === newVal) continue; + + // FIXME: ax.tick0 and dtick get filled in during plotting, and unlike other auto values + // they don't make it back into the input, so newContainer won't have them. + // similar for axis ranges for 3D + // contourcarpet doesn't HAVE zmin/zmax, they're just auto-added. It needs them. + if(key === 'tick0' || key === 'dtick') { + var tickMode = newContainer.tickmode; + if(tickMode === 'auto' || tickMode === 'array' || !tickMode) continue; + } + if(key === 'range' && newContainer.autorange) continue; + if((key === 'zmin' || key === 'zmax') && newContainer.type === 'contourcarpet') continue; + + var parts = outerparts.concat(key); + valObject = getValObject(parts); + + // in case type changed, we may not even *have* a valObject. + if(!valObject) continue; + + var valType = valObject.valType; + var i; + + var canBeDataArray = valObjectCanBeDataArray(valObject); + var wasArray = Array.isArray(oldVal); + var nowArray = Array.isArray(newVal); + + // hack for traces that modify the data in supplyDefaults, like + // converting 1D to 2D arrays, which will always create new objects + if(wasArray && nowArray) { + var inputKey = '_input_' + key; + var oldValIn = oldContainer[inputKey]; + var newValIn = newContainer[inputKey]; + if(Array.isArray(oldValIn) && oldValIn === newValIn) continue; + } + + if(newVal === undefined) { + if(canBeDataArray && wasArray) flags.calc = true; + else changed(); + } + else if(valObject._isLinkedToArray) { + var arrayEditIndices = []; + var extraIndices = false; + if(!inArray) flags.arrays[key] = arrayEditIndices; + + var minLen = Math.min(oldVal.length, newVal.length); + var maxLen = Math.max(oldVal.length, newVal.length); + if(minLen !== maxLen) { + if(valObject.editType === 'arraydraw') { + extraIndices = true; + } + else { + changed(); + continue; + } + } + + for(i = 0; i < minLen; i++) { + getDiffFlags(oldVal[i], newVal[i], parts.concat(i), + // add array indices, but not if we're already in an array + Lib.extendFlat({inArray: key, arrayIndex: i}, opts)); + } + + // put this at the end so that we know our collected array indices are sorted + // but the check for length changes happens up front so we can short-circuit + // diffing if appropriate + if(extraIndices) { + for(i = minLen; i < maxLen; i++) { + arrayEditIndices.push(i); + } + } + } + else if(!valType && Lib.isPlainObject(oldVal)) { + getDiffFlags(oldVal, newVal, parts, opts); + } + else if(canBeDataArray) { + if(wasArray && nowArray) { + + // don't try to diff two data arrays. If immutable we know the data changed, + // if not, assume it didn't and let `layout.datarevision` tell us if it did + if(immutable) { + flags.calc = true; + } + } + else if(wasArray !== nowArray) { + flags.calc = true; + } + else changed(); + } + else if(wasArray && nowArray) { + // info array, colorscale, 'any' - these are short, just stringify. + // I don't *think* that covers up any real differences post-validation, does it? + // otherwise we need to dive in 1 (info_array) or 2 (colorscale) levels and compare + // all elements. + if(oldVal.length !== newVal.length || String(oldVal) !== String(newVal)) { + changed(); + } + } + else { + changed(); + } + } + + for(key in newContainer) { + if(!(key in oldContainer)) { + valObject = getValObject(outerparts.concat(key)); + + if(valObjectCanBeDataArray(valObject) && Array.isArray(newContainer[key])) { + flags.calc = true; + return; + } + else changed(); + } + } +} + +/* + * simple diff for config - for now, just treat all changes as equivalent + */ +function diffConfig(oldConfig, newConfig) { + var key; + + for(key in oldConfig) { + var oldVal = oldConfig[key]; + var newVal = newConfig[key]; + if(oldVal !== newVal) { + if(Lib.isPlainObject(oldVal) && Lib.isPlainObject(newVal)) { + if(diffConfig(oldVal, newVal)) { + return true; + } + } + else if(Array.isArray(oldVal) && Array.isArray(newVal)) { + if(oldVal.length !== newVal.length) { + return true; + } + for(var i = 0; i < oldVal.length; i++) { + if(oldVal[i] !== newVal[i]) { + if(Lib.isPlainObject(oldVal[i]) && Lib.isPlainObject(newVal[i])) { + if(diffConfig(oldVal[i], newVal[i])) { + return true; + } + } + else { + return true; + } + } + } + } + else { + return true; + } + } + } +} + /** * Animate to a frame, sequence of frame, frame group, or frame definition * diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 07c09c6eafc..01f892f748f 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -1817,17 +1817,6 @@ axes.doTicks = function(gd, axid, skipTitle) { } } - // make sure we only have allowed options for exponents - // (others can make confusing errors) - if(!ax.tickformat) { - if(['none', 'e', 'E', 'power', 'SI', 'B'].indexOf(ax.exponentformat) === -1) { - ax.exponentformat = 'e'; - } - if(['all', 'first', 'last', 'none'].indexOf(ax.showexponent) === -1) { - ax.showexponent = 'all'; - } - } - // set scaling to pixels ax.setScale(); diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 779703a2ce4..a56d7befae1 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -9,6 +9,8 @@ 'use strict'; +var d3 = require('d3'); + var Fx = require('../../components/fx'); var dragElement = require('../../components/dragelement'); @@ -18,7 +20,13 @@ var makeDragBox = require('./dragbox').makeDragBox; module.exports = function initInteractions(gd) { var fullLayout = gd._fullLayout; - if((!fullLayout._has('cartesian') && !fullLayout._has('gl2d')) || gd._context.staticPlot) return; + if(gd._context.staticPlot) { + // this sweeps up more than just cartesian drag elements... + d3.select(gd).selectAll('.drag').remove(); + return; + } + + if(!fullLayout._has('cartesian') && !fullLayout._has('gl2d')) return; var subplots = Object.keys(fullLayout._plots || {}).sort(function(a, b) { // sort overlays last, then by x axis number, then y axis number diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index 01db927a7b9..342d5900b2e 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -398,15 +398,16 @@ module.exports = function setConvert(ax, fullLayout) { // in case the expected data isn't there, make a list of // integers based on the opposite data ax.makeCalcdata = function(trace, axLetter) { - var arrayIn, arrayOut, i; + var arrayIn, arrayOut, i, len; var cal = ax.type === 'date' && trace[axLetter + 'calendar']; if(axLetter in trace) { arrayIn = trace[axLetter]; - arrayOut = new Array(arrayIn.length); + len = trace._length || arrayIn.length; + arrayOut = new Array(len); - for(i = 0; i < arrayIn.length; i++) { + for(i = 0; i < len; i++) { arrayOut[i] = ax.d2c(arrayIn[i], 0, cal); } } @@ -418,9 +419,10 @@ module.exports = function setConvert(ax, fullLayout) { // the opposing data, for size if we have x and dx etc arrayIn = trace[{x: 'y', y: 'x'}[axLetter]]; - arrayOut = new Array(arrayIn.length); + len = trace._length || arrayIn.length; + arrayOut = new Array(len); - for(i = 0; i < arrayIn.length; i++) arrayOut[i] = v0 + i * dv; + for(i = 0; i < len; i++) arrayOut[i] = v0 + i * dv; } return arrayOut; }; diff --git a/src/plots/gl3d/layout/tick_marks.js b/src/plots/gl3d/layout/tick_marks.js index 9cdb2299687..97b4e20fed5 100644 --- a/src/plots/gl3d/layout/tick_marks.js +++ b/src/plots/gl3d/layout/tick_marks.js @@ -50,6 +50,7 @@ function computeTickMarks(scene) { if(Math.abs(axes._length) === Infinity) { ticks[i] = []; } else { + axes._input_range = axes.range.slice(); axes.range[0] = (glRange[i].lo) / scene.dataScale[i]; axes.range[1] = (glRange[i].hi) / scene.dataScale[i]; axes._m = 1.0 / (scene.dataScale[i] * glRange[i].pixelsPerDataUnit); diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index 2ee604891f0..8a4a2453cfb 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -306,9 +306,15 @@ proto.recoverContext = function() { var axisProperties = [ 'xaxis', 'yaxis', 'zaxis' ]; -function coordinateBound(axis, coord, d, bounds, calendar) { +function coordinateBound(axis, coord, len, d, bounds, calendar) { var x; - for(var i = 0; i < coord.length; ++i) { + if(!Array.isArray(coord)) { + bounds[0][d] = Math.min(bounds[0][d], 0); + bounds[1][d] = Math.max(bounds[1][d], len - 1); + return; + } + + for(var i = 0; i < (len || coord.length); ++i) { if(Array.isArray(coord[i])) { for(var j = 0; j < coord[i].length; ++j) { x = axis.d2l(coord[i][j], 0, calendar); @@ -330,9 +336,9 @@ function coordinateBound(axis, coord, d, bounds, calendar) { function computeTraceBounds(scene, trace, bounds) { var sceneLayout = scene.fullSceneLayout; - coordinateBound(sceneLayout.xaxis, trace.x, 0, bounds, trace.xcalendar); - coordinateBound(sceneLayout.yaxis, trace.y, 1, bounds, trace.ycalendar); - coordinateBound(sceneLayout.zaxis, trace.z, 2, bounds, trace.zcalendar); + coordinateBound(sceneLayout.xaxis, trace.x, trace._xlength, 0, bounds, trace.xcalendar); + coordinateBound(sceneLayout.yaxis, trace.y, trace._ylength, 1, bounds, trace.ycalendar); + coordinateBound(sceneLayout.zaxis, trace.z, trace._zlength, 2, bounds, trace.zcalendar); } proto.plot = function(sceneData, fullLayout, layout) { diff --git a/src/plots/layout_attributes.js b/src/plots/layout_attributes.js index 2cf5fac6172..1dfee32e8f7 100644 --- a/src/plots/layout_attributes.js +++ b/src/plots/layout_attributes.js @@ -182,4 +182,18 @@ module.exports = { editType: 'calc', description: 'Sets the default trace colors.' }, + datarevision: { + valType: 'any', + role: 'info', + editType: 'calc', + description: [ + 'If provided, a changed value tells `Plotly.react` that', + 'one or more data arrays has changed. This way you can modify', + 'arrays in-place rather than making a complete new copy for an', + 'incremental change.', + 'If NOT provided, `Plotly.react` assumes that data arrays are', + 'being treated as immutable, thus any data array with a', + 'different identity from its predecessor contains new data.' + ].join(' ') + } }; diff --git a/src/plots/mapbox/index.js b/src/plots/mapbox/index.js index eb1eeed7c0f..0e7b12d57d8 100644 --- a/src/plots/mapbox/index.js +++ b/src/plots/mapbox/index.js @@ -62,9 +62,6 @@ exports.plot = function plotMapbox(gd) { opts = fullLayout[id], mapbox = opts._subplot; - // copy access token to fullLayout (to handle the context case) - opts.accesstoken = accessToken; - if(!mapbox) { mapbox = createMapbox({ gd: gd, @@ -136,24 +133,17 @@ function findAccessToken(gd, mapboxIds) { // special case for Mapbox Atlas users if(context.mapboxAccessToken === '') return ''; - // first look for access token in context - var accessToken = context.mapboxAccessToken; - - // allow mapbox layout options to override it + // Take the first token we find in a mapbox subplot. + // These default to the context value but may be overridden. for(var i = 0; i < mapboxIds.length; i++) { var opts = fullLayout[mapboxIds[i]]; if(opts.accesstoken) { - accessToken = opts.accesstoken; - break; + return opts.accesstoken; } } - if(!accessToken) { - throw new Error(constants.noAccessTokenErrorMsg); - } - - return accessToken; + throw new Error(constants.noAccessTokenErrorMsg); } exports.updateFx = function(fullLayout) { diff --git a/src/plots/mapbox/layout_defaults.js b/src/plots/mapbox/layout_defaults.js index f14a3744583..7c381dd9144 100644 --- a/src/plots/mapbox/layout_defaults.js +++ b/src/plots/mapbox/layout_defaults.js @@ -20,12 +20,13 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { type: 'mapbox', attributes: layoutAttributes, handleDefaults: handleDefaults, - partition: 'y' + partition: 'y', + accessToken: layoutOut._mapboxAccessToken }); }; -function handleDefaults(containerIn, containerOut, coerce) { - coerce('accesstoken'); +function handleDefaults(containerIn, containerOut, coerce, opts) { + coerce('accesstoken', opts.accessToken); coerce('style'); coerce('center.lon'); coerce('center.lat'); diff --git a/src/plots/plots.js b/src/plots/plots.js index 32c16ac1f3e..ae049d78d5c 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -281,13 +281,21 @@ var extraFormatKeys = [ // is a list of all the transform modules invoked. // plots.supplyDefaults = function(gd) { - var oldFullLayout = gd._fullLayout || {}, - newFullLayout = gd._fullLayout = {}, - newLayout = gd.layout || {}; + var oldFullLayout = gd._fullLayout || {}; - var oldFullData = gd._fullData || [], - newFullData = gd._fullData = [], - newData = gd.data || []; + if(oldFullLayout._skipDefaults) { + delete oldFullLayout._skipDefaults; + return; + } + + var newFullLayout = gd._fullLayout = {}; + var newLayout = gd.layout || {}; + + var oldFullData = gd._fullData || []; + var newFullData = gd._fullData = []; + var newData = gd.data || []; + + var context = gd._context || {}; var i; @@ -316,6 +324,9 @@ plots.supplyDefaults = function(gd) { var formatObj = getFormatObj(gd, d3FormatKeys); + // stash the token from context so mapbox subplots can use it as default + newFullLayout._mapboxAccessToken = context.mapboxAccessToken; + // first fill in what we can of layout without looking at data // because fullData needs a few things from layout @@ -337,7 +348,7 @@ plots.supplyDefaults = function(gd) { var missingWidthOrHeight = (!newLayout.width || !newLayout.height), autosize = newFullLayout.autosize, - autosizable = gd._context && gd._context.autosizable, + autosizable = context.autosizable, initialAutoSize = missingWidthOrHeight && (autosize || autosizable); if(initialAutoSize) plots.plotAutoSize(gd, newLayout, newFullLayout); @@ -1246,6 +1257,8 @@ plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut, formatObj) { coerce('colorway'); + coerce('datarevision'); + Registry.getComponentMethod( 'calendars', 'handleDefaults' diff --git a/src/plots/polar/layout_defaults.js b/src/plots/polar/layout_defaults.js index e6dc76406f4..9e4d49ad15e 100644 --- a/src/plots/polar/layout_defaults.js +++ b/src/plots/polar/layout_defaults.js @@ -87,6 +87,7 @@ function handleDefaults(contIn, contOut, coerce, opts) { switch(axName) { case 'radialaxis': var autoRange = coerceAxis('autorange', !axOut.isValidRange(axIn.range)); + axIn.autorange = autoRange; if(autoRange) coerceAxis('rangemode'); if(autoRange === 'reversed') axOut._m = -1; diff --git a/src/traces/carpet/calc.js b/src/traces/carpet/calc.js index 3a2829b7211..0a87098ea42 100644 --- a/src/traces/carpet/calc.js +++ b/src/traces/carpet/calc.js @@ -33,13 +33,13 @@ module.exports = function calc(gd, trace) { if(trace._cheater) { var avals = aax.cheatertype === 'index' ? a.length : a; var bvals = bax.cheatertype === 'index' ? b.length : b; - trace.x = x = cheaterBasis(avals, bvals, trace.cheaterslope); + x = cheaterBasis(avals, bvals, trace.cheaterslope); } else { x = trace.x; } - trace._x = trace.x = x = clean2dArray(x); - trace._y = trace.y = y = clean2dArray(y); + trace._x = x = clean2dArray(x); + trace._y = y = clean2dArray(y); // Fill in any undefined values with elliptic smoothing. This doesn't take // into account the spacing of the values. That is, the derivatives should diff --git a/src/traces/carpet/calc_gridlines.js b/src/traces/carpet/calc_gridlines.js index 66f8456ab37..93c6021e81b 100644 --- a/src/traces/carpet/calc_gridlines.js +++ b/src/traces/carpet/calc_gridlines.js @@ -27,10 +27,7 @@ module.exports = function calcGridlines(trace, cd, axisLetter, crossAxisLetter) var crossAxis = trace[crossAxisLetter + 'axis']; if(axis.tickmode === 'array') { - axis.tickvals = []; - for(i = 0; i < data.length; i++) { - axis.tickvals.push(data[i]); - } + axis.tickvals = data.slice(); } var xcp = trace.xctrl; @@ -42,6 +39,9 @@ module.exports = function calcGridlines(trace, cd, axisLetter, crossAxisLetter) Axes.calcTicks(axis); + // don't leave tickvals in axis looking like an attribute + if(axis.tickmode === 'array') delete axis.tickvals; + // The default is an empty array that will cause the join to remove the gridline if // it's just disappeared: // axis._startline = axis._endline = []; diff --git a/src/traces/carpet/set_convert.js b/src/traces/carpet/set_convert.js index fc01f772d4a..f47cb0c52a3 100644 --- a/src/traces/carpet/set_convert.js +++ b/src/traces/carpet/set_convert.js @@ -65,8 +65,8 @@ module.exports = function setConvert(trace) { bax.c2p = function(v) { return v; }; trace.setScale = function() { - var x = trace.x; - var y = trace.y; + var x = trace._x; + var y = trace._y; // This is potentially a very expensive step! It does the bulk of the work of constructing // an expanded basis of control points. Note in particular that it overwrites the existing diff --git a/src/traces/heatmap/convert_column_xyz.js b/src/traces/heatmap/convert_column_xyz.js index 0e8d2f200e1..162d8f94e6e 100644 --- a/src/traces/heatmap/convert_column_xyz.js +++ b/src/traces/heatmap/convert_column_xyz.js @@ -70,9 +70,13 @@ module.exports = function convertColumnData(trace, ax1, ax2, var1Name, var2Name, } } + // hack for Plotly.react - save the input arrays for diffing purposes + trace['_input_' + var1Name] = trace[var1Name]; + trace['_input_' + var2Name] = trace[var2Name]; trace[var1Name] = col1vals; trace[var2Name] = col2vals; for(j = 0; j < arrayVarNames.length; j++) { + trace['_input_' + arrayVarNames[j]] = trace[arrayVarNames[j]]; trace[arrayVarNames[j]] = newArrays[j]; } if(hasColumnText) trace.text = text; diff --git a/src/traces/histogram/bin_defaults.js b/src/traces/histogram/bin_defaults.js index 97ddf0aabd0..77259579edd 100644 --- a/src/traces/histogram/bin_defaults.js +++ b/src/traces/histogram/bin_defaults.js @@ -23,8 +23,9 @@ module.exports = function handleBinDefaults(traceIn, traceOut, coerce, binDirect coerce(binDirection + 'bins.start'); coerce(binDirection + 'bins.end'); coerce(binDirection + 'bins.size'); - coerce('autobin' + binDirection); - coerce('nbins' + binDirection); + + var autobin = coerce('autobin' + binDirection); + if(autobin !== false) coerce('nbins' + binDirection); }); return traceOut; diff --git a/src/traces/histogram/clean_bins.js b/src/traces/histogram/clean_bins.js index c3cb7e5e3d0..dc322d7401d 100644 --- a/src/traces/histogram/clean_bins.js +++ b/src/traces/histogram/clean_bins.js @@ -65,11 +65,14 @@ module.exports = function cleanBins(trace, ax, binDirection) { var autoBinAttr = 'autobin' + binDirection; if(typeof trace[autoBinAttr] !== 'boolean') { - trace[autoBinAttr] = !( + trace[autoBinAttr] = trace._fullInput[autoBinAttr] = trace._input[autoBinAttr] = !( (bins.start || bins.start === 0) && (bins.end || bins.end === 0) ); } - if(!trace[autoBinAttr]) delete trace['nbins' + binDirection]; + if(!trace[autoBinAttr]) { + delete trace['nbins' + binDirection]; + delete trace._fullInput['nbins' + binDirection]; + } }; diff --git a/src/traces/ohlc/transform.js b/src/traces/ohlc/transform.js index d249ca85ba3..c569c6ede3d 100644 --- a/src/traces/ohlc/transform.js +++ b/src/traces/ohlc/transform.js @@ -213,6 +213,7 @@ exports.calcTransform = function calcTransform(gd, trace, opts) { trace.x = x; trace.y = y; trace.text = textOut; + trace._length = x.length; }; function convertTickWidth(gd, xa, trace) { diff --git a/src/traces/pie/calc.js b/src/traces/pie/calc.js index 6333373790f..a8f4baaaa34 100644 --- a/src/traces/pie/calc.js +++ b/src/traces/pie/calc.js @@ -18,7 +18,7 @@ module.exports = function calc(gd, trace) { var vals = trace.values; var hasVals = Array.isArray(vals) && vals.length; var labels = trace.labels; - var colors = trace.marker.colors; + var colors = trace.marker.colors || []; var cd = []; var fullLayout = gd._fullLayout; var colorWay = fullLayout.colorway; diff --git a/src/traces/pie/defaults.js b/src/traces/pie/defaults.js index 5881fde486a..a13e89497cc 100644 --- a/src/traces/pie/defaults.js +++ b/src/traces/pie/defaults.js @@ -34,8 +34,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout var lineWidth = coerce('marker.line.width'); if(lineWidth) coerce('marker.line.color'); - var colors = coerce('marker.colors'); - if(!Array.isArray(colors)) traceOut.marker.colors = []; + coerce('marker.colors'); coerce('scalegroup'); // TODO: hole needs to be coerced to the same value within a scaleegroup diff --git a/src/traces/scatter/calc.js b/src/traces/scatter/calc.js index 49f3105f9e9..c720b0fcf02 100644 --- a/src/traces/scatter/calc.js +++ b/src/traces/scatter/calc.js @@ -24,15 +24,12 @@ function calc(gd, trace) { var ya = Axes.getFromId(gd, trace.yaxis || 'y'); var x = xa.makeCalcdata(trace, 'x'); var y = ya.makeCalcdata(trace, 'y'); - var serieslen = Math.min(x.length, y.length); + var serieslen = trace._length; // cancel minimum tick spacings (only applies to bars and boxes) xa._minDtick = 0; ya._minDtick = 0; - if(x.length > serieslen) x.splice(serieslen, x.length - serieslen); - if(y.length > serieslen) y.splice(serieslen, y.length - serieslen); - // check whether bounds should be tight, padded, extended to zero... // most cases both should be padded on both ends, so start with that. var xOptions = {padded: true}; @@ -120,9 +117,13 @@ function calcMarkerSize(trace, serieslen) { Axes.setConvert(ax); var s = ax.makeCalcdata(trace.marker, 'size'); - if(s.length > serieslen) s.splice(serieslen, s.length - serieslen); - return s.map(markerTrans); + var sizeOut = new Array(serieslen); + for(var i = 0; i < serieslen; i++) { + sizeOut[i] = markerTrans(s[i]); + } + return sizeOut; + } else { return markerTrans(marker.size); } diff --git a/src/traces/scatter/xy_defaults.js b/src/traces/scatter/xy_defaults.js index 8fbce7402ff..27a987af471 100644 --- a/src/traces/scatter/xy_defaults.js +++ b/src/traces/scatter/xy_defaults.js @@ -23,13 +23,6 @@ module.exports = function handleXYDefaults(traceIn, traceOut, layout, coerce) { if(x) { if(y) { len = Math.min(x.length, y.length); - // TODO: not sure we should do this here... but I think - // the way it works in calc is wrong, because it'll delete data - // which could be a problem eg in streaming / editing if x and y - // come in at different times - // so we need to revisit calc before taking this out - if(len < x.length) traceOut.x = x.slice(0, len); - if(len < y.length) traceOut.y = y.slice(0, len); } else { len = x.length; @@ -44,5 +37,8 @@ module.exports = function handleXYDefaults(traceIn, traceOut, layout, coerce) { coerce('x0'); coerce('dx'); } + + traceOut._length = len; + return len; }; diff --git a/src/traces/scatter3d/defaults.js b/src/traces/scatter3d/defaults.js index 6f302fd177d..050a2dcfed3 100644 --- a/src/traces/scatter3d/defaults.js +++ b/src/traces/scatter3d/defaults.js @@ -78,10 +78,9 @@ function handleXYZDefaults(traceIn, traceOut, coerce, layout) { handleCalendarDefaults(traceIn, traceOut, ['x', 'y', 'z'], layout); if(x && y && z) { + // TODO: what happens if one is missing? len = Math.min(x.length, y.length, z.length); - if(len < x.length) traceOut.x = x.slice(0, len); - if(len < y.length) traceOut.y = y.slice(0, len); - if(len < z.length) traceOut.z = z.slice(0, len); + traceOut._xlength = traceOut._ylength = traceOut._zlength = len; } return len; diff --git a/src/traces/scattergeo/calc.js b/src/traces/scattergeo/calc.js index 5a187fe3cdd..af3af25236b 100644 --- a/src/traces/scattergeo/calc.js +++ b/src/traces/scattergeo/calc.js @@ -20,7 +20,7 @@ var _ = require('../../lib')._; module.exports = function calc(gd, trace) { var hasLocationData = Array.isArray(trace.locations); - var len = hasLocationData ? trace.locations.length : trace.lon.length; + var len = hasLocationData ? trace.locations.length : trace._length; var calcTrace = new Array(len); for(var i = 0; i < len; i++) { diff --git a/src/traces/scattergeo/defaults.js b/src/traces/scattergeo/defaults.js index 55b54ab8d6d..5e09810f39f 100644 --- a/src/traces/scattergeo/defaults.js +++ b/src/traces/scattergeo/defaults.js @@ -69,9 +69,7 @@ function handleLonLatLocDefaults(traceIn, traceOut, coerce) { lon = coerce('lon') || []; lat = coerce('lat') || []; len = Math.min(lon.length, lat.length); - - if(len < lon.length) traceOut.lon = lon.slice(0, len); - if(len < lat.length) traceOut.lat = lat.slice(0, len); + traceOut._length = len; return len; } diff --git a/src/traces/scattergl/index.js b/src/traces/scattergl/index.js index 4692c377ba6..71e36b627b2 100644 --- a/src/traces/scattergl/index.js +++ b/src/traces/scattergl/index.js @@ -53,7 +53,7 @@ function calc(container, trace) { var x = xaxis.type === 'linear' ? trace.x : xaxis.makeCalcdata(trace, 'x'); var y = yaxis.type === 'linear' ? trace.y : yaxis.makeCalcdata(trace, 'y'); - var count = (x || y).length, i, l, xx, yy; + var count = trace._length, i, xx, yy; if(!x) { x = Array(count); @@ -69,38 +69,19 @@ function calc(container, trace) { } // get log converted positions - var rawx, rawy; - if(xaxis.type === 'log') { - rawx = Array(x.length); - for(i = 0, l = x.length; i < l; i++) { - rawx[i] = x[i]; - x[i] = xaxis.d2l(x[i]); - } - } - else { - rawx = x; - for(i = 0, l = x.length; i < l; i++) { - x[i] = parseFloat(x[i]); - } - } - if(yaxis.type === 'log') { - rawy = Array(y.length); - for(i = 0, l = y.length; i < l; i++) { - rawy[i] = y[i]; - y[i] = yaxis.d2l(y[i]); - } - } - else { - rawy = y; - for(i = 0, l = y.length; i < l; i++) { - y[i] = parseFloat(y[i]); - } - } + var rawx = (xaxis.type === 'log' || x.length > count) ? x.slice(0, count) : x; + var rawy = (yaxis.type === 'log' || y.length > count) ? y.slice(0, count) : y; + + var convertX = (xaxis.type === 'log') ? xaxis.d2l : parseFloat; + var convertY = (yaxis.type === 'log') ? yaxis.d2l : parseFloat; // we need hi-precision for scatter2d positions = new Array(count * 2); for(i = 0; i < count; i++) { + x[i] = convertX(x[i]); + y[i] = convertY(y[i]); + // if no x defined, we are creating simple int sequence (API) // we use parseFloat because it gives NaN (we need that for empty values to avoid drawing lines) and it is incredibly fast xx = isNumeric(x[i]) ? +x[i] : NaN; diff --git a/src/traces/scattermapbox/defaults.js b/src/traces/scattermapbox/defaults.js index 3d8bb73348a..210f9e1d47b 100644 --- a/src/traces/scattermapbox/defaults.js +++ b/src/traces/scattermapbox/defaults.js @@ -69,9 +69,7 @@ function handleLonLatDefaults(traceIn, traceOut, coerce) { var lon = coerce('lon') || []; var lat = coerce('lat') || []; var len = Math.min(lon.length, lat.length); - - if(len < lon.length) traceOut.lon = lon.slice(0, len); - if(len < lat.length) traceOut.lat = lat.slice(0, len); + traceOut._length = len; return len; } diff --git a/src/traces/scatterpolar/calc.js b/src/traces/scatterpolar/calc.js index 18a7f3b12bc..4c600509cff 100644 --- a/src/traces/scatterpolar/calc.js +++ b/src/traces/scatterpolar/calc.js @@ -26,7 +26,7 @@ module.exports = function calc(gd, trace) { var angularAxis = fullLayout[subplotId].angularaxis; var rArray = radialAxis.makeCalcdata(trace, 'r'); var thetaArray = angularAxis.makeCalcdata(trace, 'theta'); - var len = rArray.length; + var len = trace._length; var cd = new Array(len); function c2rad(v) { @@ -53,6 +53,7 @@ module.exports = function calc(gd, trace) { if(angularAxis.type !== 'linear') { angularAxis.autorange = true; Axes.expand(angularAxis, thetaArray); + delete angularAxis.autorange; } calcColorscale(trace); diff --git a/src/traces/scatterpolar/defaults.js b/src/traces/scatterpolar/defaults.js index 294553ecb6e..5fe19016b48 100644 --- a/src/traces/scatterpolar/defaults.js +++ b/src/traces/scatterpolar/defaults.js @@ -34,8 +34,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout return; } - if(len < r.length) traceOut.r = r.slice(0, len); - if(len < theta.length) traceOut.theta = theta.slice(0, len); + traceOut._length = len; coerce('thetaunit'); coerce('mode', len < PTS_LINESONLY ? 'lines+markers' : 'lines'); diff --git a/src/traces/scatterpolargl/defaults.js b/src/traces/scatterpolargl/defaults.js index f34e1e5bce4..1c6a5cae2c3 100644 --- a/src/traces/scatterpolargl/defaults.js +++ b/src/traces/scatterpolargl/defaults.js @@ -32,8 +32,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout return; } - if(len < r.length) traceOut.r = r.slice(0, len); - if(len < theta.length) traceOut.theta = theta.slice(0, len); + traceOut._length = len; coerce('thetaunit'); coerce('mode', len < PTS_LINESONLY ? 'lines+markers' : 'lines'); diff --git a/src/traces/scatterpolargl/index.js b/src/traces/scatterpolargl/index.js index 6a7a4a1b94a..8bd78e52393 100644 --- a/src/traces/scatterpolargl/index.js +++ b/src/traces/scatterpolargl/index.js @@ -26,6 +26,9 @@ function calc(container, trace) { var thetaArray = angularAxis.makeCalcdata(trace, 'theta'); var stash = {}; + if(trace._length < rArray.length) rArray = rArray.slice(0, trace._length); + if(trace._length < thetaArray.length) thetaArray = thetaArray.slice(0, trace._length); + calcColorscales(trace); stash.r = rArray; @@ -36,6 +39,7 @@ function calc(container, trace) { if(angularAxis.type !== 'linear') { angularAxis.autorange = true; Axes.expand(angularAxis, thetaArray); + delete angularAxis.autorange; } return [{x: false, y: false, t: stash, trace: trace}]; diff --git a/src/traces/scatterternary/calc.js b/src/traces/scatterternary/calc.js index f7bcc43cbef..e7a4be1b52e 100644 --- a/src/traces/scatterternary/calc.js +++ b/src/traces/scatterternary/calc.js @@ -20,34 +20,35 @@ var dataArrays = ['a', 'b', 'c']; var arraysToFill = {a: ['b', 'c'], b: ['a', 'c'], c: ['a', 'b']}; module.exports = function calc(gd, trace) { - var ternary = gd._fullLayout[trace.subplot], - displaySum = ternary.sum, - normSum = trace.sum || displaySum; + var ternary = gd._fullLayout[trace.subplot]; + var displaySum = ternary.sum; + var normSum = trace.sum || displaySum; + var arrays = {a: trace.a, b: trace.b, c: trace.c}; var i, j, dataArray, newArray, fillArray1, fillArray2; // fill in one missing component for(i = 0; i < dataArrays.length; i++) { dataArray = dataArrays[i]; - if(trace[dataArray]) continue; + if(arrays[dataArray]) continue; - fillArray1 = trace[arraysToFill[dataArray][0]]; - fillArray2 = trace[arraysToFill[dataArray][1]]; + fillArray1 = arrays[arraysToFill[dataArray][0]]; + fillArray2 = arrays[arraysToFill[dataArray][1]]; newArray = new Array(fillArray1.length); for(j = 0; j < fillArray1.length; j++) { newArray[j] = normSum - fillArray1[j] - fillArray2[j]; } - trace[dataArray] = newArray; + arrays[dataArray] = newArray; } // make the calcdata array - var serieslen = trace.a.length; + var serieslen = trace._length; var cd = new Array(serieslen); var a, b, c, norm, x, y; for(i = 0; i < serieslen; i++) { - a = trace.a[i]; - b = trace.b[i]; - c = trace.c[i]; + a = arrays.a[i]; + b = arrays.b[i]; + c = arrays.c[i]; if(isNumeric(a) && isNumeric(b) && isNumeric(c)) { a = +a; b = +b; diff --git a/src/traces/scatterternary/defaults.js b/src/traces/scatterternary/defaults.js index cb982c46a03..a648bef144b 100644 --- a/src/traces/scatterternary/defaults.js +++ b/src/traces/scatterternary/defaults.js @@ -55,10 +55,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout return; } - // cut all data arrays down to same length - if(a && len < a.length) traceOut.a = a.slice(0, len); - if(b && len < b.length) traceOut.b = b.slice(0, len); - if(c && len < c.length) traceOut.c = c.slice(0, len); + traceOut._length = len; coerce('sum'); diff --git a/src/traces/surface/convert.js b/src/traces/surface/convert.js index 1881235686e..3c1650b3d08 100644 --- a/src/traces/surface/convert.js +++ b/src/traces/surface/convert.js @@ -45,12 +45,17 @@ proto.handlePick = function(selection) { ]; var traceCoordinate = [0, 0, 0]; - if(Array.isArray(this.data.x[0])) { + if(!Array.isArray(this.data.x)) { + traceCoordinate[0] = selectIndex[0]; + } else if(Array.isArray(this.data.x[0])) { traceCoordinate[0] = this.data.x[selectIndex[1]][selectIndex[0]]; } else { traceCoordinate[0] = this.data.x[selectIndex[0]]; } - if(Array.isArray(this.data.y[0])) { + + if(!Array.isArray(this.data.y)) { + traceCoordinate[1] = selectIndex[1]; + } else if(Array.isArray(this.data.y[0])) { traceCoordinate[1] = this.data.y[selectIndex[1]][selectIndex[0]]; } else { traceCoordinate[1] = this.data.y[selectIndex[1]]; @@ -196,7 +201,7 @@ proto.update = function(data) { zaxis = sceneLayout.zaxis, scaleFactor = scene.dataScale, xlen = z[0].length, - ylen = z.length, + ylen = data._ylength, coords = [ ndarray(new Float32Array(xlen * ylen), [xlen, ylen]), ndarray(new Float32Array(xlen * ylen), [xlen, ylen]), @@ -226,7 +231,11 @@ proto.update = function(data) { }); // coords x - if(Array.isArray(x[0])) { + if(!Array.isArray(x)) { + fill(xc, function(row) { + return xaxis.d2l(row, 0, xcalendar) * scaleFactor[0]; + }); + } else if(Array.isArray(x[0])) { fill(xc, function(row, col) { return xaxis.d2l(x[col][row], 0, xcalendar) * scaleFactor[0]; }); @@ -238,7 +247,11 @@ proto.update = function(data) { } // coords y - if(Array.isArray(y[0])) { + if(!Array.isArray(x)) { + fill(yc, function(row, col) { + return yaxis.d2l(col, 0, xcalendar) * scaleFactor[1]; + }); + } else if(Array.isArray(y[0])) { fill(yc, function(row, col) { return yaxis.d2l(y[col][row], 0, ycalendar) * scaleFactor[1]; }); diff --git a/src/traces/surface/defaults.js b/src/traces/surface/defaults.js index 434349de623..e0a41136038 100644 --- a/src/traces/surface/defaults.js +++ b/src/traces/surface/defaults.js @@ -29,30 +29,16 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout return; } - var xlen = z[0].length; - var ylen = z.length; - - coerce('x'); + var x = coerce('x'); coerce('y'); + traceOut._xlength = (Array.isArray(x) && Array.isArray(x[0])) ? z.length : z[0].length; + traceOut._ylength = z.length; + var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleTraceDefaults'); handleCalendarDefaults(traceIn, traceOut, ['x', 'y', 'z'], layout); - if(!Array.isArray(traceOut.x)) { - // build a linearly scaled x - traceOut.x = []; - for(i = 0; i < xlen; ++i) { - traceOut.x[i] = i; - } - } - coerce('text'); - if(!Array.isArray(traceOut.y)) { - traceOut.y = []; - for(i = 0; i < ylen; ++i) { - traceOut.y[i] = i; - } - } // Coerce remaining properties [ diff --git a/src/transforms/filter.js b/src/transforms/filter.js index 5f9350590b5..3b5533b4e82 100644 --- a/src/transforms/filter.js +++ b/src/transforms/filter.js @@ -219,6 +219,7 @@ exports.calcTransform = function(gd, trace, opts) { } opts._indexToPoints = indexToPoints; + trace._length = index; }; function getFilterFunc(opts, d2c, targetCalendar) { diff --git a/test/image/baselines/range_slider_multiple.png b/test/image/baselines/range_slider_multiple.png index 14740cbb86c..23af1953709 100644 Binary files a/test/image/baselines/range_slider_multiple.png and b/test/image/baselines/range_slider_multiple.png differ diff --git a/test/image/mocks/geo_first.json b/test/image/mocks/geo_first.json index fb2c2ac4e03..0cb9151c3e7 100644 --- a/test/image/mocks/geo_first.json +++ b/test/image/mocks/geo_first.json @@ -9,7 +9,8 @@ { "type": "choropleth", "locations": ["USA", "CAN", "RUS"], - "z": [0, 5, 10] + "z": [0, 5, 10], + "autocolorscale": true } ], "layout": { diff --git a/test/image/mocks/range_slider_multiple.json b/test/image/mocks/range_slider_multiple.json index 4885806a707..09f7490bd17 100644 --- a/test/image/mocks/range_slider_multiple.json +++ b/test/image/mocks/range_slider_multiple.json @@ -26,7 +26,7 @@ "anchor": "y2", "domain": [ 0.55, 1 ], "rangeslider": { - "range": [ -1, 4 ] + "range": [ -2, 4 ] } }, "yaxis": { diff --git a/test/jasmine/tests/legend_scroll_test.js b/test/jasmine/tests/legend_scroll_test.js index 64087fd14a1..89d7ed77f9a 100644 --- a/test/jasmine/tests/legend_scroll_test.js +++ b/test/jasmine/tests/legend_scroll_test.js @@ -79,7 +79,7 @@ describe('The legend', function() { var legend = getLegend(), scrollBox = getScrollBox(), legendHeight = getLegendHeight(gd), - scrollBoxYMax = gd._fullLayout.legend.height - legendHeight, + scrollBoxYMax = gd._fullLayout.legend._height - legendHeight, scrollBarYMax = legendHeight - constants.scrollBarHeight - 2 * constants.scrollBarMargin, diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index c9700131069..c91bb568de0 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -1292,21 +1292,21 @@ describe('Test lib.js:', function() { expect(this.array).toBe(out); }); - it('should ignore falsy items', function() { + it('should ignore falsy items except 0', function() { Lib.pushUnique(this.array, false); expect(this.array).toEqual(['a', 'b', 'c', { a: 'A' }]); Lib.pushUnique(this.array, undefined); expect(this.array).toEqual(['a', 'b', 'c', { a: 'A' }]); - Lib.pushUnique(this.array, 0); - expect(this.array).toEqual(['a', 'b', 'c', { a: 'A' }]); - Lib.pushUnique(this.array, null); expect(this.array).toEqual(['a', 'b', 'c', { a: 'A' }]); Lib.pushUnique(this.array, ''); expect(this.array).toEqual(['a', 'b', 'c', { a: 'A' }]); + + Lib.pushUnique(this.array, 0); + expect(this.array).toEqual(['a', 'b', 'c', { a: 'A' }, 0]); }); it('should ignore item already in array', function() { diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index 6974f8ff63a..b783f3d89ad 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -10,6 +10,8 @@ var pkg = require('../../../package.json'); var subroutines = require('@src/plot_api/subroutines'); var helpers = require('@src/plot_api/helpers'); var editTypes = require('@src/plot_api/edit_types'); +var annotations = require('@src/components/annotations'); +var images = require('@src/components/images'); var d3 = require('d3'); var createGraphDiv = require('../assets/create_graph_div'); @@ -18,7 +20,6 @@ var fail = require('../assets/fail_test'); var checkTicks = require('../assets/custom_assertions').checkTicks; var supplyAllDefaults = require('../assets/supply_defaults'); - describe('Test plot api', function() { 'use strict'; @@ -2171,6 +2172,449 @@ describe('Test plot api', function() { }); }); }); + + describe('Plotly.react', function() { + var mockedMethods = [ + 'doTraceStyle', + 'doColorBars', + 'doLegend', + 'layoutStyles', + 'doTicksRelayout', + 'doModeBar', + 'doCamera' + ]; + + var gd; + var plotCalls; + + beforeEach(function() { + gd = createGraphDiv(); + + mockedMethods.forEach(function(m) { + spyOn(subroutines, m).and.callThrough(); + subroutines[m].calls.reset(); + }); + + spyOn(annotations, 'drawOne').and.callThrough(); + spyOn(annotations, 'draw').and.callThrough(); + spyOn(images, 'draw').and.callThrough(); + }); + + afterEach(destroyGraphDiv); + + function countPlots() { + plotCalls = 0; + + gd.on('plotly_afterplot', function() { plotCalls++; }); + subroutines.layoutStyles.calls.reset(); + annotations.draw.calls.reset(); + annotations.drawOne.calls.reset(); + images.draw.calls.reset(); + } + + function countCalls(counts) { + var callsFinal = Lib.extendFlat({}, counts); + // TODO: do we really need to do layoutStyles twice in Plotly.plot? + // We get one from drawFramework and another directly from Plotly.plot. + callsFinal.layoutStyles = (counts.layoutStyles || 0) + 2 * (counts.plot || 0); + + mockedMethods.forEach(function(m) { + expect(subroutines[m]).toHaveBeenCalledTimes(callsFinal[m] || 0); + subroutines[m].calls.reset(); + }); + + expect(plotCalls).toBe(counts.plot || 0, 'calls to Plotly.plot'); + plotCalls = 0; + + // only consider annotation and image draw calls if we *don't* do a full plot. + if(!counts.plot) { + expect(annotations.draw).toHaveBeenCalledTimes(counts.annotationDraw || 0); + expect(annotations.drawOne).toHaveBeenCalledTimes(counts.annotationDrawOne || 0); + expect(images.draw).toHaveBeenCalledTimes(counts.imageDraw || 0); + } + annotations.draw.calls.reset(); + annotations.drawOne.calls.reset(); + images.draw.calls.reset(); + } + + it('should notice new data by ===, without layout.datarevision', function(done) { + var data = [{y: [1, 2, 3], mode: 'markers'}]; + var layout = {}; + + Plotly.newPlot(gd, data, layout) + .then(countPlots) + .then(function() { + expect(d3.selectAll('.point').size()).toBe(3); + + data[0].y.push(4); + return Plotly.react(gd, data, layout); + }) + .then(function() { + // didn't pick it up, as we modified in place!!! + expect(d3.selectAll('.point').size()).toBe(3); + + data[0].y = [1, 2, 3, 4, 5]; + return Plotly.react(gd, data, layout); + }) + .then(function() { + // new object, we picked it up! + expect(d3.selectAll('.point').size()).toBe(5); + + countCalls({plot: 1}); + }) + .catch(fail) + .then(done); + }); + + it('should notice new layout.datarevision', function(done) { + var data = [{y: [1, 2, 3], mode: 'markers'}]; + var layout = {datarevision: 1}; + + Plotly.newPlot(gd, data, layout) + .then(countPlots) + .then(function() { + expect(d3.selectAll('.point').size()).toBe(3); + + data[0].y.push(4); + return Plotly.react(gd, data, layout); + }) + .then(function() { + // didn't pick it up, as we didn't modify datarevision + expect(d3.selectAll('.point').size()).toBe(3); + + data[0].y.push(5); + layout.datarevision = 'bananas'; + return Plotly.react(gd, data, layout); + }) + .then(function() { + // new revision, we picked it up! + expect(d3.selectAll('.point').size()).toBe(5); + + countCalls({plot: 1}); + }) + .catch(fail) + .then(done); + }); + + it('picks up partial redraws', function(done) { + var data = [{y: [1, 2, 3], mode: 'markers'}]; + var layout = {}; + + Plotly.newPlot(gd, data, layout) + .then(countPlots) + .then(function() { + layout.title = 'XXXXX'; + layout.hovermode = 'closest'; + data[0].marker = {color: 'rgb(0, 100, 200)'}; + return Plotly.react(gd, data, layout); + }) + .then(function() { + countCalls({layoutStyles: 1, doTraceStyle: 1, doModeBar: 1}); + expect(d3.select('.gtitle').text()).toBe('XXXXX'); + var points = d3.selectAll('.point'); + expect(points.size()).toBe(3); + points.each(function() { + expect(window.getComputedStyle(this).fill).toBe('rgb(0, 100, 200)'); + }); + + layout.showlegend = true; + layout.xaxis.tick0 = 0.1; + layout.xaxis.dtick = 0.3; + return Plotly.react(gd, data, layout); + }) + .then(function() { + // legend and ticks get called initially, but then plot gets added during automargin + countCalls({doLegend: 1, doTicksRelayout: 1, plot: 1}); + + data = [{z: [[1, 2], [3, 4]], type: 'surface'}]; + layout = {}; + + return Plotly.react(gd, data, layout); + }) + .then(function() { + // we get an extra call to layoutStyles from marginPushersAgain due to the colorbar. + // Really need to simplify that pipeline... + countCalls({plot: 1, layoutStyles: 1}); + + layout.scene.camera = {up: {x: 1, y: -1, z: 0}}; + + return Plotly.react(gd, data, layout); + }) + .then(function() { + countCalls({doCamera: 1}); + + data[0].type = 'heatmap'; + delete layout.scene; + return Plotly.react(gd, data, layout); + }) + .then(function() { + countCalls({plot: 1}); + + // ideally we'd just do this with `surface` but colorbar attrs have editType 'calc' there + // TODO: can we drop them to type: 'colorbars' even for the 3D types? + data[0].colorbar = {len: 0.6}; + return Plotly.react(gd, data, layout); + }) + .then(function() { + countCalls({doColorBars: 1, plot: 1}); + }) + .catch(fail) + .then(done); + }); + + it('redraws annotations one at a time', function(done) { + var data = [{y: [1, 2, 3], mode: 'markers'}]; + var layout = {}; + var ymax; + + Plotly.newPlot(gd, data, layout) + .then(countPlots) + .then(function() { + ymax = layout.yaxis.range[1]; + + layout.annotations = [{ + x: 1, + y: 4, + text: 'Way up high', + showarrow: false + }, { + x: 1, + y: 2, + text: 'On the data', + showarrow: false + }]; + return Plotly.react(gd, data, layout); + }) + .then(function() { + // autoranged - so we get a full replot + countCalls({plot: 1}); + expect(d3.selectAll('.annotation').size()).toBe(2); + + layout.annotations[1].bgcolor = 'rgb(200, 100, 0)'; + return Plotly.react(gd, data, layout); + }) + .then(function() { + countCalls({annotationDrawOne: 1}); + expect(window.getComputedStyle(d3.select('.annotation[data-index="1"] .bg').node()).fill) + .toBe('rgb(200, 100, 0)'); + expect(layout.yaxis.range[1]).not.toBeCloseTo(ymax, 0); + + layout.annotations[0].font = {color: 'rgb(0, 255, 0)'}; + layout.annotations[1].bgcolor = 'rgb(0, 0, 255)'; + return Plotly.react(gd, data, layout); + }) + .then(function() { + countCalls({annotationDrawOne: 2}); + expect(window.getComputedStyle(d3.select('.annotation[data-index="0"] text').node()).fill) + .toBe('rgb(0, 255, 0)'); + expect(window.getComputedStyle(d3.select('.annotation[data-index="1"] .bg').node()).fill) + .toBe('rgb(0, 0, 255)'); + + Lib.extendFlat(layout.annotations[0], {yref: 'paper', y: 0.8}); + + return Plotly.react(gd, data, layout); + }) + .then(function() { + countCalls({plot: 1}); + expect(layout.yaxis.range[1]).toBeCloseTo(ymax, 0); + }) + .catch(fail) + .then(done); + }); + + it('redraws images all at once', function(done) { + var data = [{y: [1, 2, 3], mode: 'markers'}]; + var layout = {}; + var jsLogo = 'https://images.plot.ly/language-icons/api-home/js-logo.png'; + + var x, y, height, width; + + Plotly.newPlot(gd, data, layout) + .then(countPlots) + .then(function() { + layout.images = [{ + source: jsLogo, + xref: 'paper', + yref: 'paper', + x: 0.1, + y: 0.1, + sizex: 0.2, + sizey: 0.2 + }, { + source: jsLogo, + xref: 'x', + yref: 'y', + x: 1, + y: 2, + sizex: 1, + sizey: 1 + }]; + Plotly.react(gd, data, layout); + }) + .then(function() { + countCalls({imageDraw: 1}); + expect(d3.selectAll('image').size()).toBe(2); + + var n = d3.selectAll('image').node(); + x = n.attributes.x.value; + y = n.attributes.y.value; + height = n.attributes.height.value; + width = n.attributes.width.value; + + layout.images[0].y = 0.8; + layout.images[0].sizey = 0.4; + Plotly.react(gd, data, layout); + }) + .then(function() { + countCalls({imageDraw: 1}); + var n = d3.selectAll('image').node(); + expect(n.attributes.x.value).toBe(x); + expect(n.attributes.width.value).toBe(width); + expect(n.attributes.y.value).not.toBe(y); + expect(n.attributes.height.value).not.toBe(height); + }) + .catch(fail) + .then(done); + }); + + it('can change config, and always redraws', function(done) { + var data = [{y: [1, 2, 3]}]; + var layout = {}; + + Plotly.newPlot(gd, data, layout) + .then(countPlots) + .then(function() { + expect(d3.selectAll('.drag').size()).toBe(11); + expect(d3.selectAll('.gtitle').size()).toBe(0); + + return Plotly.react(gd, data, layout, {editable: true}); + }) + .then(function() { + expect(d3.selectAll('.drag').size()).toBe(11); + expect(d3.selectAll('.gtitle').text()).toBe('Click to enter Plot title'); + countCalls({plot: 1}); + + return Plotly.react(gd, data, layout, {staticPlot: true}); + }) + .then(function() { + expect(d3.selectAll('.drag').size()).toBe(0); + expect(d3.selectAll('.gtitle').size()).toBe(0); + countCalls({plot: 1}); + + return Plotly.react(gd, data, layout, {}); + }) + .then(function() { + expect(d3.selectAll('.drag').size()).toBe(11); + expect(d3.selectAll('.gtitle').size()).toBe(0); + countCalls({plot: 1}); + }) + .catch(fail) + .then(done); + }); + + it('can put polar plots into staticPlot mode', function(done) { + // tested separately since some of the relevant code is actually + // in cartesian/graph_interact... hopefully we'll fix that + // sometime and the test will still pass. + var data = [{r: [1, 2, 3], theta: [0, 120, 240], type: 'scatterpolar'}]; + var layout = {}; + + Plotly.newPlot(gd, data, layout) + .then(countPlots) + .then(function() { + expect(d3.select(gd).selectAll('.drag').size()).toBe(3); + + return Plotly.react(gd, data, layout, {staticPlot: true}); + }) + .then(function() { + expect(d3.select(gd).selectAll('.drag').size()).toBe(0); + + return Plotly.react(gd, data, layout, {}); + }) + .then(function() { + expect(d3.select(gd).selectAll('.drag').size()).toBe(3); + }) + .catch(fail) + .then(done); + }); + + it('can change frames without redrawing', function(done) { + var data = [{y: [1, 2, 3]}]; + var layout = {}; + var frames = [{name: 'frame1'}]; + + Plotly.newPlot(gd, {data: data, layout: layout, frames: frames}) + .then(countPlots) + .then(function() { + var frameData = gd._transitionData._frames; + expect(frameData.length).toBe(1); + expect(frameData[0].name).toBe('frame1'); + + frames[0].name = 'frame2'; + return Plotly.react(gd, {data: data, layout: layout, frames: frames}); + }) + .then(function() { + countCalls({}); + var frameData = gd._transitionData._frames; + expect(frameData.length).toBe(1); + expect(frameData[0].name).toBe('frame2'); + }) + .catch(fail) + .then(done); + }); + + var mockList = [ + ['1', require('@mocks/1.json')], + ['4', require('@mocks/4.json')], + ['5', require('@mocks/5.json')], + ['10', require('@mocks/10.json')], + ['11', require('@mocks/11.json')], + ['17', require('@mocks/17.json')], + ['21', require('@mocks/21.json')], + ['22', require('@mocks/22.json')], + ['airfoil', require('@mocks/airfoil.json')], + ['annotations-autorange', require('@mocks/annotations-autorange.json')], + ['axes_enumerated_ticks', require('@mocks/axes_enumerated_ticks.json')], + ['axes_visible-false', require('@mocks/axes_visible-false.json')], + ['bar_and_histogram', require('@mocks/bar_and_histogram.json')], + ['binding', require('@mocks/binding.json')], + ['cheater_smooth', require('@mocks/cheater_smooth.json')], + ['finance_style', require('@mocks/finance_style.json')], + ['geo_first', require('@mocks/geo_first.json')], + ['gl2d_line_dash', require('@mocks/gl2d_line_dash.json')], + ['gl2d_parcoords_2', require('@mocks/gl2d_parcoords_2.json')], + ['gl2d_pointcloud-basic', require('@mocks/gl2d_pointcloud-basic.json')], + ['gl3d_world-cals', require('@mocks/gl3d_world-cals.json')], + ['gl3d_set-ranges', require('@mocks/gl3d_set-ranges.json')], + ['glpolar_style', require('@mocks/glpolar_style.json')], + ['layout_image', require('@mocks/layout_image.json')], + ['layout-colorway', require('@mocks/layout-colorway.json')], + ['polar_categories', require('@mocks/polar_categories.json')], + ['polar_direction', require('@mocks/polar_direction.json')], + ['range_selector_style', require('@mocks/range_selector_style.json')], + ['range_slider_multiple', require('@mocks/range_slider_multiple.json')], + ['sankey_energy', require('@mocks/sankey_energy.json')], + ['table_wrapped_birds', require('@mocks/table_wrapped_birds.json')], + ['ternary_fill', require('@mocks/ternary_fill.json')], + ['text_chart_arrays', require('@mocks/text_chart_arrays.json')], + ['updatemenus', require('@mocks/updatemenus.json')], + ['violins', require('@mocks/violins.json')], + ['world-cals', require('@mocks/world-cals.json')] + ]; + + mockList.forEach(function(mockSpec) { + it('can redraw "' + mockSpec[0] + '" with no changes as a noop', function(done) { + var mock = mockSpec[1]; + + Plotly.newPlot(gd, mock) + .then(countPlots) + .then(function() { return Plotly.react(gd, mock); }) + .then(function() { countCalls({}); }) + .catch(fail) + .then(done); + }); + }); + }); }); describe('plot_api helpers', function() { diff --git a/test/jasmine/tests/range_selector_test.js b/test/jasmine/tests/range_selector_test.js index 9070d9c0eca..3a29cabac23 100644 --- a/test/jasmine/tests/range_selector_test.js +++ b/test/jasmine/tests/range_selector_test.js @@ -472,7 +472,7 @@ describe('range selector interactions:', function() { function checkActiveButton(activeIndex, msg) { d3.selectAll('.button').each(function(d, i) { - expect(d.isActive).toBe(activeIndex === i, msg + ': button #' + i); + expect(d._isActive).toBe(activeIndex === i, msg + ': button #' + i); }); } @@ -481,7 +481,7 @@ describe('range selector interactions:', function() { var rect = d3.select(this).select('rect'); expect(rect.node().style.fill).toEqual( - d.isActive ? activeColor : bgColor + d._isActive ? activeColor : bgColor ); }); } diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index 94ca4160da6..d88987fde43 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -445,7 +445,7 @@ describe('the range slider', function() { it('should not mutate layoutIn', function() { var layoutIn = { xaxis: { rangeslider: { visible: true }} }, layoutOut = { xaxis: { rangeslider: {}} }, - expected = { xaxis: { rangeslider: { visible: true }} }; + expected = { xaxis: { rangeslider: { visible: true}} }; _supply(layoutIn, layoutOut, 'xaxis'); expect(layoutIn).toEqual(expected); @@ -457,7 +457,6 @@ describe('the range slider', function() { expected = { visible: true, autorange: true, - range: [-1, 6], thickness: 0.15, bgcolor: '#fff', borderwidth: 0, @@ -475,7 +474,6 @@ describe('the range slider', function() { expected = { visible: true, autorange: true, - range: [-1, 6], thickness: 0.15, bgcolor: '#fff', borderwidth: 0, @@ -507,7 +505,6 @@ describe('the range slider', function() { expected = { visible: true, autorange: true, - range: [-1, 6], thickness: 0.15, bgcolor: '#fff', borderwidth: 0, @@ -519,34 +516,12 @@ describe('the range slider', function() { expect(layoutOut.xaxis.rangeslider).toEqual(expected); }); - it('should expand the rangeslider range to axis range', function() { - var layoutIn = { xaxis: { rangeslider: { range: [5, 6] } } }, - layoutOut = { xaxis: { range: [1, 10], type: 'linear'} }, - expected = { - visible: true, - autorange: false, - range: [1, 10], - thickness: 0.15, - bgcolor: '#fff', - borderwidth: 0, - bordercolor: '#444', - _input: layoutIn.xaxis.rangeslider - }; - - _supply(layoutIn, layoutOut, 'xaxis'); - - // don't compare the whole layout, because we had to run setConvert which - // attaches all sorts of other stuff to xaxis - expect(layoutOut.xaxis.rangeslider).toEqual(expected); - }); - it('should set autorange to true when range input is invalid', function() { var layoutIn = { xaxis: { rangeslider: { range: 'not-gonna-work'}} }, layoutOut = { xaxis: {} }, expected = { visible: true, autorange: true, - range: [-1, 6], thickness: 0.15, bgcolor: '#fff', borderwidth: 0, @@ -729,6 +704,17 @@ describe('the range slider', function() { .then(function() { assertRange([-0.26, 4.26], [-0.26, 4.26]); + // smaller than xaxis.range - won't be accepted + return Plotly.relayout(gd, {'xaxis.rangeslider.range': [0, 2]}); + }) + .then(function() { + assertRange([-0.26, 4.26], [-0.26, 4.26]); + + // will be accepted (and autorange is disabled by impliedEdits) + return Plotly.relayout(gd, {'xaxis.rangeslider.range': [-2, 12]}); + }) + .then(function() { + assertRange([-0.26, 4.26], [-2, 12]); }) .then(done); }); diff --git a/test/jasmine/tests/scattergeo_test.js b/test/jasmine/tests/scattergeo_test.js index 1c9e107e020..b7368d1aa8c 100644 --- a/test/jasmine/tests/scattergeo_test.js +++ b/test/jasmine/tests/scattergeo_test.js @@ -26,18 +26,21 @@ describe('Test scattergeo defaults', function() { traceOut = {}; }); - it('should slice lat if it it longer than lon', function() { + it('should not slice lat if it it longer than lon', function() { + // this is handled at the calc step now via _length. traceIn = { lon: [-75], lat: [45, 45, 45] }; ScatterGeo.supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.lat).toEqual([45]); + expect(traceOut.lat).toEqual([45, 45, 45]); expect(traceOut.lon).toEqual([-75]); + expect(traceOut._length).toBe(1); }); it('should slice lon if it it longer than lat', function() { + // this is handled at the calc step now via _length. traceIn = { lon: [-75, -75, -75], lat: [45] @@ -45,7 +48,8 @@ describe('Test scattergeo defaults', function() { ScatterGeo.supplyDefaults(traceIn, traceOut, defaultColor, layout); expect(traceOut.lat).toEqual([45]); - expect(traceOut.lon).toEqual([-75]); + expect(traceOut.lon).toEqual([-75, -75, -75]); + expect(traceOut._length).toBe(1); }); it('should not coerce lat and lon if locations is valid', function() { diff --git a/test/jasmine/tests/scattermapbox_test.js b/test/jasmine/tests/scattermapbox_test.js index ccc6dbef587..bb4874239fd 100644 --- a/test/jasmine/tests/scattermapbox_test.js +++ b/test/jasmine/tests/scattermapbox_test.js @@ -38,24 +38,28 @@ describe('scattermapbox defaults', function() { return traceOut; } - it('should truncate \'lon\' if longer than \'lat\'', function() { + it('should not truncate \'lon\' if longer than \'lat\'', function() { + // this is handled at the calc step now via _length. var fullTrace = _supply({ lon: [1, 2, 3], lat: [2, 3] }); - expect(fullTrace.lon).toEqual([1, 2]); + expect(fullTrace.lon).toEqual([1, 2, 3]); expect(fullTrace.lat).toEqual([2, 3]); + expect(fullTrace._length).toBe(2); }); - it('should truncate \'lat\' if longer than \'lon\'', function() { + it('should not truncate \'lat\' if longer than \'lon\'', function() { + // this is handled at the calc step now via _length. var fullTrace = _supply({ lon: [1, 2, 3], lat: [2, 3, 3, 5] }); expect(fullTrace.lon).toEqual([1, 2, 3]); - expect(fullTrace.lat).toEqual([2, 3, 3]); + expect(fullTrace.lat).toEqual([2, 3, 3, 5]); + expect(fullTrace._length).toBe(3); }); it('should set \'visible\' to false if \'lat\' and/or \'lon\' has zero length', function() { diff --git a/test/jasmine/tests/scatterpolar_test.js b/test/jasmine/tests/scatterpolar_test.js index 1b46f59d9c4..58477040103 100644 --- a/test/jasmine/tests/scatterpolar_test.js +++ b/test/jasmine/tests/scatterpolar_test.js @@ -18,24 +18,28 @@ describe('Test scatterpolar trace defaults:', function() { ScatterPolar.supplyDefaults(traceIn, traceOut, '#444', layout || {}); } - it('should truncate *r* when longer than *theta*', function() { + it('should not truncate *r* when longer than *theta*', function() { + // this is handled at the calc step now via _length. _supply({ r: [1, 2, 3, 4, 5], theta: [1, 2, 3] }); - expect(traceOut.r).toEqual([1, 2, 3]); + expect(traceOut.r).toEqual([1, 2, 3, 4, 5]); expect(traceOut.theta).toEqual([1, 2, 3]); + expect(traceOut._length).toBe(3); }); - it('should truncate *theta* when longer than *r*', function() { + it('should not truncate *theta* when longer than *r*', function() { + // this is handled at the calc step now via _length. _supply({ r: [1, 2, 3], theta: [1, 2, 3, 4, 5] }); expect(traceOut.r).toEqual([1, 2, 3]); - expect(traceOut.theta).toEqual([1, 2, 3]); + expect(traceOut.theta).toEqual([1, 2, 3, 4, 5]); + expect(traceOut._length).toBe(3); }); }); diff --git a/test/jasmine/tests/scatterternary_test.js b/test/jasmine/tests/scatterternary_test.js index bbd1d6b7003..be24bbb5814 100644 --- a/test/jasmine/tests/scatterternary_test.js +++ b/test/jasmine/tests/scatterternary_test.js @@ -101,7 +101,8 @@ describe('scatterternary defaults', function() { expect(traceOut.visible).toBe(false); }); - it('should truncate data arrays to the same length (\'c\' is shortest case)', function() { + it('should not truncate data arrays to the same length (\'c\' is shortest case)', function() { + // this is handled at the calc step now via _length. traceIn = { a: [1, 2, 3], b: [1, 2], @@ -109,12 +110,14 @@ describe('scatterternary defaults', function() { }; supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.a).toEqual([1]); - expect(traceOut.b).toEqual([1]); + expect(traceOut.a).toEqual([1, 2, 3]); + expect(traceOut.b).toEqual([1, 2]); expect(traceOut.c).toEqual([1]); + expect(traceOut._length).toBe(1); }); - it('should truncate data arrays to the same length (\'a\' is shortest case)', function() { + it('should not truncate data arrays to the same length (\'a\' is shortest case)', function() { + // this is handled at the calc step now via _length. traceIn = { a: [1], b: [1, 2, 3], @@ -123,11 +126,13 @@ describe('scatterternary defaults', function() { supplyDefaults(traceIn, traceOut, defaultColor, layout); expect(traceOut.a).toEqual([1]); - expect(traceOut.b).toEqual([1]); - expect(traceOut.c).toEqual([1]); + expect(traceOut.b).toEqual([1, 2, 3]); + expect(traceOut.c).toEqual([1, 2]); + expect(traceOut._length).toBe(1); }); - it('should truncate data arrays to the same length (\'a\' is shortest case)', function() { + it('should not truncate data arrays to the same length (\'a\' is shortest case)', function() { + // this is handled at the calc step now via _length. traceIn = { a: [1, 2], b: [1], @@ -135,9 +140,10 @@ describe('scatterternary defaults', function() { }; supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.a).toEqual([1]); + expect(traceOut.a).toEqual([1, 2]); expect(traceOut.b).toEqual([1]); - expect(traceOut.c).toEqual([1]); + expect(traceOut.c).toEqual([1, 2, 3]); + expect(traceOut._length).toBe(1); }); it('should include \'name\' in \'hoverinfo\' default if multi trace graph', function() { @@ -218,32 +224,39 @@ describe('scatterternary calc', function() { trace = { subplot: 'ternary', - sum: 1 + sum: 1, + _length: 3 }; }); + function get(cd, component) { + return cd.map(function(v) { + return v[component]; + }); + } + it('should fill in missing component (case \'c\')', function() { trace.a = [0.1, 0.3, 0.6]; trace.b = [0.3, 0.6, 0.1]; - calc(gd, trace); - expect(trace.c).toBeCloseToArray([0.6, 0.1, 0.3]); + cd = calc(gd, trace); + expect(get(cd, 'c')).toBeCloseToArray([0.6, 0.1, 0.3]); }); it('should fill in missing component (case \'b\')', function() { trace.a = [0.1, 0.3, 0.6]; trace.c = [0.1, 0.3, 0.2]; - calc(gd, trace); - expect(trace.b).toBeCloseToArray([0.8, 0.4, 0.2]); + cd = calc(gd, trace); + expect(get(cd, 'b')).toBeCloseToArray([0.8, 0.4, 0.2]); }); it('should fill in missing component (case \'a\')', function() { trace.b = [0.1, 0.3, 0.6]; trace.c = [0.8, 0.4, 0.1]; - calc(gd, trace); - expect(trace.a).toBeCloseToArray([0.1, 0.3, 0.3]); + cd = calc(gd, trace); + expect(get(cd, 'a')).toBeCloseToArray([0.1, 0.3, 0.3]); }); it('should skip over non-numeric values', function() { diff --git a/test/jasmine/tests/surface_test.js b/test/jasmine/tests/surface_test.js index b7d23716bc7..00cfe59ff05 100644 --- a/test/jasmine/tests/surface_test.js +++ b/test/jasmine/tests/surface_test.js @@ -25,14 +25,15 @@ describe('Test surface', function() { expect(traceOut.visible).toBe(false); }); - it('should fill \'x\' and \'y\' if not provided', function() { + it('should NOT fill \'x\' and \'y\' if not provided', function() { + // this happens later on now traceIn = { z: [[1, 2, 3], [2, 1, 2]] }; supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.x).toEqual([0, 1, 2]); - expect(traceOut.y).toEqual([0, 1]); + expect(traceOut.x).toBeUndefined(); + expect(traceOut.y).toBeUndefined(); }); it('should coerce \'project\' if contours or highlight lines are enabled', function() { diff --git a/test/jasmine/tests/toimage_test.js b/test/jasmine/tests/toimage_test.js index 3f1181a4c26..40e5a4601a9 100644 --- a/test/jasmine/tests/toimage_test.js +++ b/test/jasmine/tests/toimage_test.js @@ -149,7 +149,7 @@ describe('Plotly.toImage', function() { .then(function() { return Plotly.toImage(gd, {format: 'png', imageDataOnly: true}); }) .then(function(d) { expect(d.indexOf('data:image/')).toBe(-1); - expect(d.length).toBeWithin(50000, 5e3, 'png image length'); + expect(d.length).toBeWithin(52500, 7500, 'png image length'); }) .then(function() { return Plotly.toImage(gd, {format: 'jpeg', imageDataOnly: true}); }) .then(function(d) {