diff --git a/dist/translation-keys.txt b/dist/translation-keys.txt index 42154ee4f3b..1f81c5ed361 100644 --- a/dist/translation-keys.txt +++ b/dist/translation-keys.txt @@ -1,12 +1,12 @@ Autoscale // components/modebar/buttons.js:139 Box Select // components/modebar/buttons.js:103 -Click to enter Colorscale title // plots/plots.js:437 -Click to enter Component A title // plots/ternary/ternary.js:386 -Click to enter Component B title // plots/ternary/ternary.js:400 -Click to enter Component C title // plots/ternary/ternary.js:411 +Click to enter Colorscale title // plots/plots.js:303 +Click to enter Component A title // plots/ternary/ternary.js:392 +Click to enter Component B title // plots/ternary/ternary.js:406 +Click to enter Component C title // plots/ternary/ternary.js:417 Click to enter Plot title // plot_api/plot_api.js:579 -Click to enter X axis title // plots/plots.js:435 -Click to enter Y axis title // plots/plots.js:436 +Click to enter X axis title // plots/plots.js:301 +Click to enter Y axis title // plots/plots.js:302 Compare data on hover // components/modebar/buttons.js:167 Double-click on legend to isolate one trace // components/legend/handle_click.js:90 Double-click to zoom back out // plots/cartesian/dragbox.js:299 @@ -17,18 +17,18 @@ Lasso Select // components/modebar/but Orbital rotation // components/modebar/buttons.js:279 Pan // components/modebar/buttons.js:94 Produced with Plotly // components/modebar/modebar.js:256 -Reset // components/modebar/buttons.js:432 +Reset // components/modebar/buttons.js:431 Reset axes // components/modebar/buttons.js:148 -Reset camera to default // components/modebar/buttons.js:314 -Reset camera to last save // components/modebar/buttons.js:322 -Reset view // components/modebar/buttons.js:583 -Reset views // components/modebar/buttons.js:529 +Reset camera to default // components/modebar/buttons.js:313 +Reset camera to last save // components/modebar/buttons.js:321 +Reset view // components/modebar/buttons.js:582 +Reset views // components/modebar/buttons.js:528 Show closest data on hover // components/modebar/buttons.js:157 Snapshot succeeded // components/modebar/buttons.js:66 Sorry, there was a problem downloading your snapshot! // components/modebar/buttons.js:69 Taking snapshot - this may take a few seconds // components/modebar/buttons.js:57 -Toggle Spike Lines // components/modebar/buttons.js:548 -Toggle show closest data on hover // components/modebar/buttons.js:353 +Toggle Spike Lines // components/modebar/buttons.js:547 +Toggle show closest data on hover // components/modebar/buttons.js:352 Turntable rotation // components/modebar/buttons.js:288 Zoom // components/modebar/buttons.js:85 Zoom in // components/modebar/buttons.js:121 @@ -52,5 +52,5 @@ q1: // traces/box/calc.js:130 q3: // traces/box/calc.js:131 source: // traces/sankey/plot.js:140 target: // traces/sankey/plot.js:141 -trace // plots/plots.js:439 +trace // plots/plots.js:305 upper fence: // traces/box/calc.js:135 \ No newline at end of file diff --git a/src/components/annotations/calc_autorange.js b/src/components/annotations/calc_autorange.js index 8da79d83448..436125c4801 100644 --- a/src/components/annotations/calc_autorange.js +++ b/src/components/annotations/calc_autorange.js @@ -23,19 +23,19 @@ module.exports = function calcAutorange(gd) { var annotationAxes = {}; annotationList.forEach(function(ann) { - annotationAxes[ann.xref] = true; - annotationAxes[ann.yref] = true; + annotationAxes[ann.xref] = 1; + annotationAxes[ann.yref] = 1; }); - var autorangedAnnos = Axes.list(gd).filter(function(ax) { - return ax.autorange && annotationAxes[ax._id]; - }); - if(!autorangedAnnos.length) return; - - return Lib.syncOrAsync([ - draw, - annAutorange - ], gd); + for(var axId in annotationAxes) { + var ax = Axes.getFromId(gd, axId); + if(ax && ax.autorange) { + return Lib.syncOrAsync([ + draw, + annAutorange + ], gd); + } + } }; function annAutorange(gd) { diff --git a/src/components/annotations/index.js b/src/components/annotations/index.js index a3cd7893545..339c740f49b 100644 --- a/src/components/annotations/index.js +++ b/src/components/annotations/index.js @@ -18,6 +18,7 @@ module.exports = { layoutAttributes: require('./attributes'), supplyLayoutDefaults: require('./defaults'), + includeBasePlot: require('../../plots/cartesian/include_components')('annotations'), calcAutorange: require('./calc_autorange'), draw: drawModule.draw, diff --git a/src/components/annotations3d/index.js b/src/components/annotations3d/index.js index 4ff43fa9f56..48bbd3d7717 100644 --- a/src/components/annotations3d/index.js +++ b/src/components/annotations3d/index.js @@ -8,6 +8,9 @@ 'use strict'; +var Registry = require('../../registry'); +var Lib = require('../../lib'); + module.exports = { moduleType: 'component', name: 'annotations3d', @@ -20,7 +23,24 @@ module.exports = { layoutAttributes: require('./attributes'), handleDefaults: require('./defaults'), + includeBasePlot: includeGL3D, convert: require('./convert'), draw: require('./draw') }; + +function includeGL3D(layoutIn, layoutOut) { + var GL3D = Registry.subplotsRegistry.gl3d; + if(!GL3D) return; + + var attrRegex = GL3D.attrRegex; + + var keys = Object.keys(layoutIn); + for(var i = 0; i < keys.length; i++) { + var k = keys[i]; + if(attrRegex.test(k) && (layoutIn[k].annotations || []).length) { + Lib.pushUnique(layoutOut._basePlotModules, GL3D); + Lib.pushUnique(layoutOut._subplots.gl3d, k); + } + } +} diff --git a/src/components/colorbar/draw.js b/src/components/colorbar/draw.js index 57f853006da..0f2a561a027 100644 --- a/src/components/colorbar/draw.js +++ b/src/components/colorbar/draw.js @@ -341,10 +341,13 @@ module.exports = function draw(gd, id) { } } - container.selectAll('.cbfills,.cblines,.cbaxis') + container.selectAll('.cbfills,.cblines') .attr('transform', 'translate(0,' + Math.round(gs.h * (1 - cbAxisOut.domain[1])) + ')'); + cbAxisOut._axislayer.attr('transform', 'translate(0,' + + Math.round(-gs.t) + ')'); + var fills = container.select('.cbfills') .selectAll('rect.cbfill') .data(filllevels); @@ -433,7 +436,7 @@ module.exports = function draw(gd, id) { selection: d3.select(gd).selectAll('g.' + cbAxisOut._id + 'tick'), side: opts.titleside, offsetLeft: gs.l, - offsetTop: gs.t, + offsetTop: 0, maxShift: fullLayout.width }, attributes: {x: x, y: y, 'text-anchor': 'middle'}, diff --git a/src/components/images/index.js b/src/components/images/index.js index 3a4269ef8df..f9bec099aae 100644 --- a/src/components/images/index.js +++ b/src/components/images/index.js @@ -14,6 +14,7 @@ module.exports = { layoutAttributes: require('./attributes'), supplyLayoutDefaults: require('./defaults'), + includeBasePlot: require('../../plots/cartesian/include_components')('images'), draw: require('./draw'), diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 681516f5b86..3432194f22e 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -11,7 +11,7 @@ var Plotly = require('../../plotly'); var Plots = require('../../plots/plots'); -var Axes = require('../../plots/cartesian/axes'); +var axisIds = require('../../plots/cartesian/axis_ids'); var Lib = require('../../lib'); var downloadImage = require('../../snapshot/download'); var Icons = require('../../../build/ploticon'); @@ -175,15 +175,15 @@ modeBarButtons.hoverCompareCartesian = { }; function handleCartesian(gd, ev) { - var button = ev.currentTarget, - astr = button.getAttribute('data-attr'), - val = button.getAttribute('data-val') || true, - fullLayout = gd._fullLayout, - aobj = {}, - axList = Axes.list(gd, null, true), - ax, - allEnabled = 'on', - i; + var button = ev.currentTarget; + var astr = button.getAttribute('data-attr'); + var val = button.getAttribute('data-val') || true; + var fullLayout = gd._fullLayout; + var aobj = {}; + var axList = axisIds.list(gd, null, true); + var allEnabled = 'on'; + + var ax, i; if(astr === 'zoom') { var mag = (val === 'in') ? 0.5 : 2, @@ -293,12 +293,11 @@ modeBarButtons.tableRotation = { }; function handleDrag3d(gd, ev) { - var button = ev.currentTarget, - attr = button.getAttribute('data-attr'), - val = button.getAttribute('data-val') || true, - fullLayout = gd._fullLayout, - sceneIds = Plots.getSubplotIds(fullLayout, 'gl3d'), - layoutUpdate = {}; + var button = ev.currentTarget; + var attr = button.getAttribute('data-attr'); + var val = button.getAttribute('data-val') || true; + var sceneIds = gd._fullLayout._subplots.gl3d; + var layoutUpdate = {}; var parts = attr.split('.'); @@ -326,11 +325,11 @@ modeBarButtons.resetCameraLastSave3d = { }; function handleCamera3d(gd, ev) { - var button = ev.currentTarget, - attr = button.getAttribute('data-attr'), - fullLayout = gd._fullLayout, - sceneIds = Plots.getSubplotIds(fullLayout, 'gl3d'), - aobj = {}; + var button = ev.currentTarget; + var attr = button.getAttribute('data-attr'); + var fullLayout = gd._fullLayout; + var sceneIds = fullLayout._subplots.gl3d; + var aobj = {}; for(var i = 0; i < sceneIds.length; i++) { var sceneId = sceneIds[i], @@ -360,19 +359,19 @@ modeBarButtons.hoverClosest3d = { }; function handleHover3d(gd, ev) { - var button = ev.currentTarget, - val = button._previousVal || false, - layout = gd.layout, - fullLayout = gd._fullLayout, - sceneIds = Plots.getSubplotIds(fullLayout, 'gl3d'); + var button = ev.currentTarget; + var val = button._previousVal || false; + var layout = gd.layout; + var fullLayout = gd._fullLayout; + var sceneIds = fullLayout._subplots.gl3d; - var axes = ['xaxis', 'yaxis', 'zaxis'], - spikeAttrs = ['showspikes', 'spikesides', 'spikethickness', 'spikecolor']; + var axes = ['xaxis', 'yaxis', 'zaxis']; + var spikeAttrs = ['showspikes', 'spikesides', 'spikethickness', 'spikecolor']; // initialize 'current spike' object to be stored in the DOM - var currentSpikes = {}, - axisSpikes = {}, - layoutUpdate = {}; + var currentSpikes = {}; + var axisSpikes = {}; + var layoutUpdate = {}; if(val) { layoutUpdate = Lib.extendDeep(layout, val); @@ -452,7 +451,7 @@ function handleGeo(gd, ev) { var attr = button.getAttribute('data-attr'); var val = button.getAttribute('data-val') || true; var fullLayout = gd._fullLayout; - var geoIds = Plots.getSubplotIds(fullLayout, 'geo'); + var geoIds = fullLayout._subplots.geo; for(var i = 0; i < geoIds.length; i++) { var id = geoIds[i]; @@ -563,11 +562,11 @@ modeBarButtons.toggleSpikelines = { }; function setSpikelineVisibility(gd) { - var fullLayout = gd._fullLayout, - axList = Axes.list(gd, null, true), - ax, - axName, - aobj = {}; + var fullLayout = gd._fullLayout; + var axList = axisIds.list(gd, null, true); + var aobj = {}; + + var ax, axName; for(var i = 0; i < axList.length; i++) { ax = axList[i]; @@ -590,7 +589,7 @@ modeBarButtons.resetViewMapbox = { function resetView(gd, subplotType) { var fullLayout = gd._fullLayout; - var subplotIds = Plots.getSubplotIds(fullLayout, subplotType); + var subplotIds = fullLayout._subplots[subplotType]; var aObj = {}; for(var i = 0; i < subplotIds.length; i++) { diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index bedee3dcb7d..706b9e254b0 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -9,7 +9,7 @@ 'use strict'; -var Axes = require('../../plots/cartesian/axes'); +var axisIds = require('../../plots/cartesian/axis_ids'); var scatterSubTypes = require('../../traces/scatter/subtypes'); var Registry = require('../../registry'); @@ -150,17 +150,15 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd) { } function areAllAxesFixed(fullLayout) { - var axList = Axes.list({_fullLayout: fullLayout}, null, true); - var allFixed = true; + var axList = axisIds.list({_fullLayout: fullLayout}, null, true); for(var i = 0; i < axList.length; i++) { if(!axList[i].fixedrange) { - allFixed = false; - break; + return false; } } - return allFixed; + return true; } // look for traces that support selection diff --git a/src/components/shapes/index.js b/src/components/shapes/index.js index 444ede3cd59..be1a542bbdc 100644 --- a/src/components/shapes/index.js +++ b/src/components/shapes/index.js @@ -17,6 +17,7 @@ module.exports = { layoutAttributes: require('./attributes'), supplyLayoutDefaults: require('./defaults'), + includeBasePlot: require('../../plots/cartesian/include_components')('shapes'), calcAutorange: require('./calc_autorange'), draw: drawModule.draw, diff --git a/src/constants/alignment.js b/src/constants/alignment.js index 1ed61b55ce1..66ff87c2947 100644 --- a/src/constants/alignment.js +++ b/src/constants/alignment.js @@ -38,5 +38,12 @@ module.exports = { // of the font, and according to wikipedia: // an "average" font might have a cap height of 70% of the em // https://en.wikipedia.org/wiki/Em_(typography)#History - MID_SHIFT: 0.35 + MID_SHIFT: 0.35, + + OPPOSITE_SIDE: { + left: 'right', + right: 'left', + top: 'bottom', + bottom: 'top' + } }; diff --git a/src/lib/index.js b/src/lib/index.js index 88c7cc495c9..e7acdce54b7 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -858,3 +858,29 @@ lib.templateString = function(string, obj) { return getterCache[key]() || ''; }); }; + +/* + * alphanumeric string sort, tailored for subplot IDs like scene2, scene10, x10y13 etc + */ +var char0 = 48; +var char9 = 57; +lib.subplotSort = function(a, b) { + var l = Math.min(a.length, b.length) + 1; + var numA = 0; + var numB = 0; + for(var i = 0; i < l; i++) { + var charA = a.charCodeAt(i) || 0; + var charB = b.charCodeAt(i) || 0; + var isNumA = charA >= char0 && charA <= char9; + var isNumB = charB >= char0 && charB <= char9; + + if(isNumA) numA = 10 * numA + charA - char0; + if(isNumB) numB = 10 * numB + charB - char0; + + if(!isNumA || !isNumB) { + if(numA !== numB) return numA - numB; + if(charA !== charB) return charA - charB; + } + } + return numB - numA; +}; diff --git a/src/plot_api/helpers.js b/src/plot_api/helpers.js index 66baee79534..8261969c4f8 100644 --- a/src/plot_api/helpers.js +++ b/src/plot_api/helpers.js @@ -15,7 +15,9 @@ var m4FromQuat = require('gl-mat4/fromQuat'); var Registry = require('../registry'); var Lib = require('../lib'); var Plots = require('../plots/plots'); -var Axes = require('../plots/cartesian/axes'); +var AxisIds = require('../plots/cartesian/axis_ids'); +var cleanId = AxisIds.cleanId; +var getFromTrace = AxisIds.getFromTrace; var Color = require('../components/color'); @@ -45,38 +47,78 @@ exports.cleanLayout = function(layout) { if(!layout.yaxis) layout.yaxis = layout.yaxis1; delete layout.yaxis1; } + if(layout.scene1) { + if(!layout.scene) layout.scene = layout.scene1; + delete layout.scene1; + } - var axList = Axes.list({_fullLayout: layout}); - for(i = 0; i < axList.length; i++) { - var ax = axList[i]; - if(ax.anchor && ax.anchor !== 'free') { - ax.anchor = Axes.cleanId(ax.anchor); - } - if(ax.overlaying) ax.overlaying = Axes.cleanId(ax.overlaying); + var axisAttrRegex = (Plots.subplotsRegistry.cartesian || {}).attrRegex; + var sceneAttrRegex = (Plots.subplotsRegistry.gl3d || {}).attrRegex; - // old method of axis type - isdate and islog (before category existed) - if(!ax.type) { - if(ax.isdate) ax.type = 'date'; - else if(ax.islog) ax.type = 'log'; - else if(ax.isdate === false && ax.islog === false) ax.type = 'linear'; - } - if(ax.autorange === 'withzero' || ax.autorange === 'tozero') { - ax.autorange = true; - ax.rangemode = 'tozero'; + var keys = Object.keys(layout); + for(i = 0; i < keys.length; i++) { + var key = keys[i]; + + // modifications to cartesian axes + if(axisAttrRegex && axisAttrRegex.test(key)) { + var ax = layout[key]; + if(ax.anchor && ax.anchor !== 'free') { + ax.anchor = cleanId(ax.anchor); + } + if(ax.overlaying) ax.overlaying = cleanId(ax.overlaying); + + // old method of axis type - isdate and islog (before category existed) + if(!ax.type) { + if(ax.isdate) ax.type = 'date'; + else if(ax.islog) ax.type = 'log'; + else if(ax.isdate === false && ax.islog === false) ax.type = 'linear'; + } + if(ax.autorange === 'withzero' || ax.autorange === 'tozero') { + ax.autorange = true; + ax.rangemode = 'tozero'; + } + delete ax.islog; + delete ax.isdate; + delete ax.categories; // replaced by _categories + + // prune empty domain arrays made before the new nestedProperty + if(emptyContainer(ax, 'domain')) delete ax.domain; + + // autotick -> tickmode + if(ax.autotick !== undefined) { + if(ax.tickmode === undefined) { + ax.tickmode = ax.autotick ? 'auto' : 'linear'; + } + delete ax.autotick; + } } - delete ax.islog; - delete ax.isdate; - delete ax.categories; // replaced by _categories - // prune empty domain arrays made before the new nestedProperty - if(emptyContainer(ax, 'domain')) delete ax.domain; + // modifications for 3D scenes + else if(sceneAttrRegex && sceneAttrRegex.test(key)) { + var scene = layout[key]; + + // clean old Camera coords + var cameraposition = scene.cameraposition; + + if(Array.isArray(cameraposition) && cameraposition[0].length === 4) { + var rotation = cameraposition[0], + center = cameraposition[1], + radius = cameraposition[2], + mat = m4FromQuat([], rotation), + eye = []; - // autotick -> tickmode - if(ax.autotick !== undefined) { - if(ax.tickmode === undefined) { - ax.tickmode = ax.autotick ? 'auto' : 'linear'; + for(j = 0; j < 3; ++j) { + eye[j] = center[j] + radius * mat[2 + 4 * j]; + } + + scene.camera = { + eye: {x: eye[0], y: eye[1], z: eye[2]}, + center: {x: center[0], y: center[1], z: center[2]}, + up: {x: mat[1], y: mat[5], z: mat[9]} + }; + + delete scene.cameraposition; } - delete ax.autotick; } } @@ -139,43 +181,6 @@ exports.cleanLayout = function(layout) { */ if(layout.dragmode === 'rotate') layout.dragmode = 'orbit'; - // cannot have scene1, numbering goes scene, scene2, scene3... - if(layout.scene1) { - if(!layout.scene) layout.scene = layout.scene1; - delete layout.scene1; - } - - /* - * Clean up Scene layouts - */ - var sceneIds = Plots.getSubplotIds(layout, 'gl3d'); - for(i = 0; i < sceneIds.length; i++) { - var scene = layout[sceneIds[i]]; - - // clean old Camera coords - var cameraposition = scene.cameraposition; - - if(Array.isArray(cameraposition) && cameraposition[0].length === 4) { - var rotation = cameraposition[0], - center = cameraposition[1], - radius = cameraposition[2], - mat = m4FromQuat([], rotation), - eye = []; - - for(j = 0; j < 3; ++j) { - eye[j] = center[i] + radius * mat[2 + 4 * j]; - } - - scene.camera = { - eye: {x: eye[0], y: eye[1], z: eye[2]}, - center: {x: center[0], y: center[1], z: center[2]}, - up: {x: mat[1], y: mat[5], z: mat[9]} - }; - - delete scene.cameraposition; - } - } - // sanitize rgb(fractions) and rgba(fractions) that old tinycolor // supported, but new tinycolor does not because they're not valid css Color.clean(layout); @@ -187,7 +192,7 @@ function cleanAxRef(container, attr) { var valIn = container[attr], axLetter = attr.charAt(0); if(valIn && valIn !== 'paper') { - container[attr] = Axes.cleanId(valIn, axLetter); + container[attr] = cleanId(valIn, axLetter); } } @@ -268,8 +273,8 @@ exports.cleanData = function(data, existingData) { } // axis ids x1 -> x, y1-> y - if(trace.xaxis) trace.xaxis = Axes.cleanId(trace.xaxis, 'x'); - if(trace.yaxis) trace.yaxis = Axes.cleanId(trace.yaxis, 'y'); + if(trace.xaxis) trace.xaxis = cleanId(trace.xaxis, 'x'); + if(trace.yaxis) trace.yaxis = cleanId(trace.yaxis, 'y'); // scene ids scene1 -> scene if(Registry.traceIs(trace, 'gl3d') && trace.scene) { @@ -529,7 +534,7 @@ exports.clearAxisTypes = function(gd, traces, layoutUpdate) { for(var i = 0; i < traces.length; i++) { var trace = gd._fullData[i]; for(var j = 0; j < 3; j++) { - var ax = Axes.getFromTrace(gd, trace, axLetters[j]); + var ax = getFromTrace(gd, trace, axLetters[j]); // do not clear log type - that's never an auto result so must have been intentional if(ax && ax.type !== 'log') { diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 22016271911..c6e9802cf46 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -276,7 +276,7 @@ Plotly.plot = function(gd, data, layout, config) { return; } - var subplots = Plots.getSubplotIds(fullLayout, 'cartesian'); + var subplots = fullLayout._subplots.cartesian; var modules = fullLayout._modules; var setPositionsArray = []; @@ -2052,7 +2052,7 @@ function _relayout(gd, aobj) { } // figure out if we need to recalculate axis constraints - var constraints = fullLayout._axisConstraintGroups; + var constraints = fullLayout._axisConstraintGroups || []; for(axId in rangesAltered) { for(i = 0; i < constraints.length; i++) { var group = constraints[i]; diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index 78fae75a679..539a96cf152 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -21,6 +21,7 @@ var Titles = require('../components/titles'); var ModeBar = require('../components/modebar'); var initInteractions = require('../plots/cartesian/graph_interact'); var cartesianConstants = require('../plots/cartesian/constants'); +var alignmentConstants = require('../constants/alignment'); exports.layoutStyles = function(gd) { return Lib.syncOrAsync([Plots.doAutoMargin, exports.lsInner], gd); @@ -53,8 +54,44 @@ exports.lsInner = function(gd) { var hasSVGCartesian = fullLayout._has('cartesian'); var i; - // clear axis line positions, to be set in the subplot loop below - for(i = 0; i < axList.length; i++) axList[i]._linepositions = {}; + function getLinePosition(ax, counterAx, side) { + var lwHalf = ax._lw / 2; + + if(ax._id.charAt(0) === 'x') { + if(!counterAx) return gs.t + gs.h * (1 - (ax.position || 0)) + (lwHalf % 1); + else if(side === 'top') return counterAx._offset - pad - lwHalf; + return counterAx._offset + counterAx._length + pad + lwHalf; + } + + if(!counterAx) return gs.l + gs.w * (ax.position || 0) + (lwHalf % 1); + else if(side === 'right') return counterAx._offset + counterAx._length + pad + lwHalf; + return counterAx._offset - pad - lwHalf; + } + + // some preparation of axis position info + for(i = 0; i < axList.length; i++) { + var ax = axList[i]; + + // reset scale in case the margins have changed + ax.setScale(); + + var counterAx = ax._anchorAxis; + + // clear axis line positions, to be set in the subplot loop below + ax._linepositions = {}; + + // stash crispRounded linewidth so we don't need to pass gd all over the place + ax._lw = Drawing.crispRound(gd, ax.linewidth, 1); + + // figure out the main axis line and main mirror line position. + // it's easier to follow the logic if we handle these separately from + // ax._linepositions, which are really only used by mirror=allticks + // for the non-main-subplot ticks. + ax._mainLinePosition = getLinePosition(ax, counterAx, ax.side); + ax._mainMirrorPosition = (ax.mirror && counterAx) ? + getLinePosition(ax, counterAx, + alignmentConstants.OPPOSITE_SIDE[ax.side]) : null; + } fullLayout._paperdiv .style({ @@ -129,16 +166,11 @@ exports.lsInner = function(gd) { fullLayout._plots[subplot].bg = d3.select(this); }); - var freeFinished = {}; subplotSelection.each(function(subplot) { var plotinfo = fullLayout._plots[subplot]; var xa = plotinfo.xaxis; var ya = plotinfo.yaxis; - // reset scale in case the margins have changed - xa.setScale(); - ya.setScale(); - if(plotinfo.bg && hasSVGCartesian) { plotinfo.bg .call(Drawing.setRect, @@ -193,18 +225,38 @@ exports.lsInner = function(gd) { // to DRY up Drawing.setClipUrl calls downstream plotinfo.layerClipId = layerClipId; - var xIsFree = !xa._anchorAxis; - var showFreeX = xIsFree && !freeFinished[xa._id]; - var showBottom = shouldShowLine(xa, ya, 'bottom'); - var showTop = shouldShowLine(xa, ya, 'top'); + // figure out extra axis line and tick positions as needed + if(!hasSVGCartesian) return; - var yIsFree = !ya._anchorAxis; - var showFreeY = yIsFree && !freeFinished[ya._id]; - var showLeft = shouldShowLine(ya, xa, 'left'); - var showRight = shouldShowLine(ya, xa, 'right'); + var xLinesXLeft, xLinesXRight, xLinesYBottom, xLinesYTop, + leftYLineWidth, rightYLineWidth; + var yLinesYBottom, yLinesYTop, yLinesXLeft, yLinesXRight, + connectYBottom, connectYTop; + var extraSubplot; - var xlw = Drawing.crispRound(gd, xa.linewidth, 1); - var ylw = Drawing.crispRound(gd, ya.linewidth, 1); + function xLinePath(y) { + return 'M' + xLinesXLeft + ',' + y + 'H' + xLinesXRight; + } + + function xLinePathFree(y) { + return 'M' + xa._offset + ',' + y + 'h' + xa._length; + } + + function yLinePath(x) { + return 'M' + x + ',' + yLinesYTop + 'V' + yLinesYBottom; + } + + function yLinePathFree(x) { + return 'M' + x + ',' + ya._offset + 'v' + ya._length; + } + + function mainPath(ax, pathFn, pathFnFree) { + if(!ax.showline || subplot !== ax._mainSubplot) return ''; + if(!ax._anchorAxis) return pathFnFree(ax._mainLinePosition); + var out = pathFn(ax._mainLinePosition); + if(ax.mirror) out += pathFn(ax._mainMirrorPosition); + return out; + } /* * x lines get longer where they meet y lines, to make a crisp corner. @@ -220,15 +272,33 @@ exports.lsInner = function(gd) { * ----- * x2 */ - var leftYLineWidth = findCounterAxisLineWidth(gd, xa, ylw, showLeft, 'left', axList); - var xLinesXLeft = (!xIsFree && leftYLineWidth) ? - (-pad - leftYLineWidth) : 0; - var rightYLineWidth = findCounterAxisLineWidth(gd, xa, ylw, showRight, 'right', axList); - var xLinesXRight = xa._length + ((!xIsFree && rightYLineWidth) ? - (pad + rightYLineWidth) : 0); - var xLinesYFree = gs.h * (1 - (xa.position || 0)) + ((xlw / 2) % 1); - var xLinesYBottom = ya._length + pad + xlw / 2; - var xLinesYTop = -pad - xlw / 2; + if(shouldShowLinesOrTicks(xa, subplot)) { + leftYLineWidth = findCounterAxisLineWidth(xa, 'left', ya, axList); + xLinesXLeft = xa._offset - (leftYLineWidth ? (pad + leftYLineWidth) : 0); + rightYLineWidth = findCounterAxisLineWidth(xa, 'right', ya, axList); + xLinesXRight = xa._offset + xa._length + (rightYLineWidth ? (pad + rightYLineWidth) : 0); + xLinesYBottom = getLinePosition(xa, ya, 'bottom'); + xLinesYTop = getLinePosition(xa, ya, 'top'); + + // save axis line positions for extra ticks to reference + // each subplot that gets ticks from "allticks" gets an entry: + // [left or bottom, right or top] + extraSubplot = (!xa._anchorAxis || subplot !== xa._mainSubplot); + if(extraSubplot && xa.ticks && xa.mirror === 'allticks') { + xa._linepositions[subplot] = [xLinesYBottom, xLinesYTop]; + } + + var xPath = mainPath(xa, xLinePath, xLinePathFree); + if(extraSubplot && xa.showline && (xa.mirror === 'all' || xa.mirror === 'allticks')) { + xPath += xLinePath(xLinesYBottom) + xLinePath(xLinesYTop); + } + + plotinfo.xlines + .attr('d', xPath || 'M0,0') + .style('stroke-width', xa._lw + 'px') + .call(Color.stroke, xa.showline ? + xa.linecolor : 'rgba(0,0,0,0)'); + } /* * y lines that meet x axes get longer only by margin.pad, because @@ -241,105 +311,30 @@ exports.lsInner = function(gd) { * | * +----- */ - var connectYBottom = !yIsFree && findCounterAxisLineWidth( - gd, ya, xlw, showBottom, 'bottom', axList); - var yLinesYBottom = ya._length + (connectYBottom ? pad : 0); - var connectYTop = !yIsFree && findCounterAxisLineWidth( - gd, ya, xlw, showTop, 'top', axList); - var yLinesYTop = connectYTop ? -pad : 0; - var yLinesXFree = gs.w * (ya.position || 0) + ((ylw / 2) % 1); - var yLinesXLeft = -pad - ylw / 2; - var yLinesXRight = xa._length + pad + ylw / 2; - - function xLinePath(y, showThis) { - if(!showThis) return ''; - return 'M' + xLinesXLeft + ',' + y + 'H' + xLinesXRight; - } - - function yLinePath(x, showThis) { - if(!showThis) return ''; - return 'M' + x + ',' + yLinesYTop + 'V' + yLinesYBottom; - } - - // save axis line positions for ticks, draggers, etc to reference - // each subplot gets an entry: - // [left or bottom, right or top, free, main] - // main is the position at which to draw labels and draggers, if any - xa._linepositions[subplot] = [ - showBottom ? xLinesYBottom : undefined, - showTop ? xLinesYTop : undefined, - showFreeX ? xLinesYFree : undefined - ]; - if(xa._anchorAxis === ya) { - xa._linepositions[subplot][3] = xa.side === 'top' ? - xLinesYTop : xLinesYBottom; - } - else if(showFreeX) { - xa._linepositions[subplot][3] = xLinesYFree; - } - - ya._linepositions[subplot] = [ - showLeft ? yLinesXLeft : undefined, - showRight ? yLinesXRight : undefined, - showFreeY ? yLinesXFree : undefined - ]; - if(ya._anchorAxis === xa) { - ya._linepositions[subplot][3] = ya.side === 'right' ? - yLinesXRight : yLinesXLeft; - } - else if(showFreeY) { - ya._linepositions[subplot][3] = yLinesXFree; - } + if(shouldShowLinesOrTicks(ya, subplot)) { + connectYBottom = findCounterAxisLineWidth(ya, 'bottom', xa, axList); + yLinesYBottom = ya._offset + ya._length + (connectYBottom ? pad : 0); + connectYTop = findCounterAxisLineWidth(ya, 'top', xa, axList); + yLinesYTop = ya._offset - (connectYTop ? pad : 0); + yLinesXLeft = getLinePosition(ya, xa, 'left'); + yLinesXRight = getLinePosition(ya, xa, 'right'); + + extraSubplot = (!ya._anchorAxis || subplot !== xa._mainSubplot); + if(extraSubplot && ya.ticks && ya.mirror === 'allticks') { + ya._linepositions[subplot] = [yLinesXLeft, yLinesXRight]; + } - // translate all the extra stuff to have the - // same origin as the plot area or axes - var origin = 'translate(' + xa._offset + ',' + ya._offset + ')'; - var originX = origin; - var originY = origin; - if(showFreeX) { - originX = 'translate(' + xa._offset + ',' + gs.t + ')'; - xLinesYTop += ya._offset - gs.t; - xLinesYBottom += ya._offset - gs.t; - } - if(showFreeY) { - originY = 'translate(' + gs.l + ',' + ya._offset + ')'; - yLinesXLeft += xa._offset - gs.l; - yLinesXRight += xa._offset - gs.l; - } + var yPath = mainPath(ya, yLinePath, yLinePathFree); + if(extraSubplot && ya.showline && (ya.mirror === 'all' || ya.mirror === 'allticks')) { + yPath += yLinePath(yLinesXLeft) + yLinePath(yLinesXRight); + } - if(hasSVGCartesian) { - plotinfo.xlines - .attr('transform', originX) - .attr('d', ( - xLinePath(xLinesYBottom, showBottom) + - xLinePath(xLinesYTop, showTop) + - xLinePath(xLinesYFree, showFreeX)) || - // so it doesn't barf with no lines shown - 'M0,0') - .style('stroke-width', xlw + 'px') - .call(Color.stroke, xa.showline ? - xa.linecolor : 'rgba(0,0,0,0)'); plotinfo.ylines - .attr('transform', originY) - .attr('d', ( - yLinePath(yLinesXLeft, showLeft) + - yLinePath(yLinesXRight, showRight) + - yLinePath(yLinesXFree, showFreeY)) || - 'M0,0') - .style('stroke-width', ylw + 'px') + .attr('d', yPath || 'M0,0') + .style('stroke-width', ya._lw + 'px') .call(Color.stroke, ya.showline ? ya.linecolor : 'rgba(0,0,0,0)'); } - - plotinfo.xaxislayer.attr('transform', originX); - plotinfo.yaxislayer.attr('transform', originY); - plotinfo.gridlayer.attr('transform', origin); - plotinfo.zerolinelayer.attr('transform', origin); - plotinfo.draglayer.attr('transform', origin); - - // mark free axes as displayed, so we don't draw them again - if(showFreeX) freeFinished[xa._id] = 1; - if(showFreeY) freeFinished[ya._id] = 1; }); Plotly.Axes.makeClipPaths(gd); @@ -349,60 +344,52 @@ exports.lsInner = function(gd) { return gd._promises.length && Promise.all(gd._promises); }; -function shouldShowLine(ax, counterAx, side) { - return (ax._anchorAxis === counterAx && (ax.mirror || ax.side === side)) || - ax.mirror === 'all' || ax.mirror === 'allticks' || - (ax.mirrors && ax.mirrors[counterAx._id + side]); +function shouldShowLinesOrTicks(ax, subplot) { + return (ax.ticks || ax.showline) && + (subplot === ax._mainSubplot || ax.mirror === 'all' || ax.mirror === 'allticks'); } -function findCounterAxes(gd, ax, axList) { - var counterAxes = []; - var anchorAx = ax._anchorAxis; - if(anchorAx) { - var counterMain = anchorAx._mainAxis; - if(counterAxes.indexOf(counterMain) === -1) { - counterAxes.push(counterMain); - for(var i = 0; i < axList.length; i++) { - if(axList[i].overlaying === counterMain._id && - counterAxes.indexOf(axList[i]) === -1 - ) { - counterAxes.push(axList[i]); - } - } - } +/* + * should we draw a line on counterAx at this side of ax? + * It's assumed that counterAx is known to overlay the subplot we're working on + * but it may not be its main axis. + */ +function shouldShowLineThisSide(ax, side, counterAx) { + // does counterAx get a line at all? + if(!counterAx.showline || !counterAx._lw) return false; + + // are we drawing *all* lines for counterAx? + if(counterAx.mirror === 'all' || counterAx.mirror === 'allticks') return true; + + var anchorAx = counterAx._anchorAxis; + + // is this a free axis? free axes can only have a subplot side-line with all(ticks)? mirroring + if(!anchorAx) return false; + + // in order to handle cases where the user forgot to anchor this axis correctly + // (because its default anchor has the same domain on the relevant end) + // check whether the relevant position is the same. + var sideIndex = alignmentConstants.FROM_BL[side]; + if(counterAx.side === side) { + return anchorAx.domain[sideIndex] === ax.domain[sideIndex]; } - return counterAxes; + return counterAx.mirror && anchorAx.domain[1 - sideIndex] === ax.domain[1 - sideIndex]; } -function findLineWidth(gd, axes, side) { - for(var i = 0; i < axes.length; i++) { - var ax = axes[i]; - var anchorAx = ax._anchorAxis; - if(anchorAx && shouldShowLine(ax, anchorAx, side)) { - return Drawing.crispRound(gd, ax.linewidth); - } +/* + * Is there another axis intersecting `side` end of `ax`? + * First look at `counterAx` (the axis for this subplot), + * then at all other potential counteraxes on or overlaying this subplot. + * Take the line width from the first one that has a line. + */ +function findCounterAxisLineWidth(ax, side, counterAx, axList) { + if(shouldShowLineThisSide(ax, side, counterAx)) { + return counterAx._lw; } -} - -function findCounterAxisLineWidth(gd, ax, subplotCounterLineWidth, - subplotCounterIsShown, side, axList) { - if(subplotCounterIsShown) return subplotCounterLineWidth; - - var i; - - // find all counteraxes for this one, then of these, find the - // first one that has a visible line on this side - var mainAxis = ax._mainAxis; - var counterAxes = findCounterAxes(gd, mainAxis, axList); - - var lineWidth = findLineWidth(gd, counterAxes, side); - if(lineWidth) return lineWidth; - - for(i = 0; i < axList.length; i++) { - if(axList[i].overlaying === mainAxis._id) { - counterAxes = findCounterAxes(gd, axList[i], axList); - lineWidth = findLineWidth(gd, counterAxes, side); - if(lineWidth) return lineWidth; + for(var i = 0; i < axList.length; i++) { + var axi = axList[i]; + if(axi._mainAxis === counterAx._mainAxis && shouldShowLineThisSide(ax, side, axi)) { + return axi._lw; } } return 0; @@ -503,12 +490,12 @@ exports.doModeBar = function(gd) { }; exports.doCamera = function(gd) { - var fullLayout = gd._fullLayout, - sceneIds = Plots.getSubplotIds(fullLayout, 'gl3d'); + var fullLayout = gd._fullLayout; + var sceneIds = fullLayout._subplots.gl3d; for(var i = 0; i < sceneIds.length; i++) { - var sceneLayout = fullLayout[sceneIds[i]], - scene = sceneLayout._scene; + var sceneLayout = fullLayout[sceneIds[i]]; + var scene = sceneLayout._scene; scene.setCamera(sceneLayout.camera); } diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 957762ec52b..135e81f72a4 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -34,9 +34,6 @@ var MID_SHIFT = require('../../constants/alignment').MID_SHIFT; var axes = module.exports = {}; -axes.layoutAttributes = require('./layout_attributes'); -axes.supplyLayoutDefaults = require('./layout_defaults'); - axes.setConvert = require('./set_convert'); var autoType = require('./axis_autotype'); @@ -60,10 +57,10 @@ axes.getFromTrace = axisIds.getFromTrace; * Only required if it's different from `dflt` */ axes.coerceRef = function(containerIn, containerOut, gd, attr, dflt, extraOption) { - var axLetter = attr.charAt(attr.length - 1), - axlist = axes.listIds(gd, axLetter), - refAttr = attr + 'ref', - attrDef = {}; + var axLetter = attr.charAt(attr.length - 1); + var axlist = gd._fullLayout._subplots[axLetter + 'axis']; + var refAttr = attr + 'ref'; + var attrDef = {}; if(!dflt) dflt = axlist[0] || extraOption; if(!extraOption) extraOption = dflt; @@ -1601,95 +1598,25 @@ axes.getTickFormat = function(ax) { return tickstop ? tickstop.value : ax.tickformat; }; -axes.subplotMatch = /^x([0-9]*)y([0-9]*)$/; - -// getSubplots - extract all combinations of axes we need to make plots for +// getSubplots - extract all subplot IDs we need // as an array of items like 'xy', 'x2y', 'x2y2'... // sorted by x (x,x2,x3...) then y // optionally restrict to only subplots containing axis object ax -// looks both for combinations of x and y found in the data -// and at axes and their anchors axes.getSubplots = function(gd, ax) { - var subplots = []; - var i, j, sp; - - // look for subplots in the data - var data = gd._fullData || gd.data || []; - - for(i = 0; i < data.length; i++) { - var trace = data[i]; - - if(trace.visible === false || trace.visible === 'legendonly' || - !(Registry.traceIs(trace, 'cartesian') || Registry.traceIs(trace, 'gl2d')) - ) continue; - - var xId = trace.xaxis || 'x', - yId = trace.yaxis || 'y'; - sp = xId + yId; - - if(subplots.indexOf(sp) === -1) subplots.push(sp); - } - - // look for subplots in the axes/anchors, so that we at least draw all axes - var axesList = axes.list(gd, '', true); - - function hasAx2(sp, ax2) { - return sp.indexOf(ax2._id) !== -1; - } - - for(i = 0; i < axesList.length; i++) { - var ax2 = axesList[i], - ax2Letter = ax2._id.charAt(0), - ax3Id = (ax2.anchor === 'free') ? - ((ax2Letter === 'x') ? 'y' : 'x') : - ax2.anchor, - ax3 = axes.getFromId(gd, ax3Id); + var subplotObj = gd._fullLayout._subplots; + var allSubplots = subplotObj.cartesian.concat(subplotObj.gl2d || []); - // look if ax2 is already represented in the data - var foundAx2 = false; - for(j = 0; j < subplots.length; j++) { - if(hasAx2(subplots[j], ax2)) { - foundAx2 = true; - break; - } - } - - // ignore free axes that already represented in the data - if(ax2.anchor === 'free' && foundAx2) continue; - - // ignore anchor-less axes - if(!ax3) continue; - - sp = (ax2Letter === 'x') ? - ax2._id + ax3._id : - ax3._id + ax2._id; - - if(subplots.indexOf(sp) === -1) subplots.push(sp); - } - - // filter invalid subplots - var spMatch = axes.subplotMatch, - allSubplots = []; - - for(i = 0; i < subplots.length; i++) { - sp = subplots[i]; - if(spMatch.test(sp)) allSubplots.push(sp); - } + var out = ax ? axes.findSubplotsWithAxis(allSubplots, ax) : allSubplots; - // sort the subplot ids - allSubplots.sort(function(a, b) { - var aMatch = a.match(spMatch), - bMatch = b.match(spMatch); + out.sort(function(a, b) { + var aParts = a.substr(1).split('y'); + var bParts = b.substr(1).split('y'); - if(aMatch[1] === bMatch[1]) { - return +(aMatch[2] || 1) - (bMatch[2] || 1); - } - - return +(aMatch[1]||0) - (bMatch[1]||0); + if(aParts[0] === bParts[0]) return +aParts[1] - +bParts[1]; + return +aParts[0] - +bParts[0]; }); - if(ax) return axes.findSubplotsWithAxis(allSubplots, ax); - return allSubplots; + return out; }; // find all subplots with axis 'ax' @@ -1847,7 +1774,7 @@ axes.doTicks = function(gd, axid, skipTitle) { if(axLetter === 'x') { sides = ['bottom', 'top']; transfn = function(d) { - return 'translate(' + ax.l2p(d.x) + ',0)'; + return 'translate(' + (ax._offset + ax.l2p(d.x)) + ',0)'; }; tickpathfn = function(shift, len) { if(ax._counterangle) { @@ -1860,7 +1787,7 @@ axes.doTicks = function(gd, axid, skipTitle) { else if(axLetter === 'y') { sides = ['left', 'right']; transfn = function(d) { - return 'translate(0,' + ax.l2p(d.x) + ')'; + return 'translate(0,' + (ax._offset + ax.l2p(d.x)) + ')'; }; tickpathfn = function(shift, len) { if(ax._counterangle) { @@ -2269,13 +2196,15 @@ axes.doTicks = function(gd, axid, skipTitle) { } function drawGrid(plotinfo, counteraxis, subplot) { - var gridcontainer = plotinfo.gridlayer, - zlcontainer = plotinfo.zerolinelayer, - gridvals = plotinfo['hidegrid' + axLetter] ? [] : valsClipped, - gridpath = ax._gridpath || - 'M0,0' + ((axLetter === 'x') ? 'v' : 'h') + counteraxis._length, - grid = gridcontainer.selectAll('path.' + gcls) - .data((ax.showgrid === false) ? [] : gridvals, datafn); + var gridcontainer = plotinfo.gridlayer.selectAll('.' + axid); + var zlcontainer = plotinfo.zerolinelayer; + var gridvals = plotinfo['hidegrid' + axLetter] ? [] : valsClipped; + var gridpath = ax._gridpath || ((axLetter === 'x' ? + ('M0,' + counteraxis._offset + 'v') : + ('M' + counteraxis._offset + ',0h') + ) + counteraxis._length); + var grid = gridcontainer.selectAll('path.' + gcls) + .data((ax.showgrid === false) ? [] : gridvals, datafn); grid.enter().append('path').classed(gcls, 1) .classed('crisp', 1) .attr('d', gridpath) @@ -2304,10 +2233,18 @@ axes.doTicks = function(gd, axid, skipTitle) { (ax.type === 'linear' || ax.type === '-') && gridvals.length && (hasBarsOrFill || clipEnds({x: 0}) || !ax.showline); var zl = zlcontainer.selectAll('path.' + zcls) - .data(showZl ? [{x: 0}] : []); + .data(showZl ? [{x: 0, id: axid}] : []); zl.enter().append('path').classed(zcls, 1).classed('zl', 1) .classed('crisp', 1) - .attr('d', gridpath); + .attr('d', gridpath) + .each(function() { + // use the fact that only one element can enter to trigger a sort. + // If several zerolines enter at the same time we will sort once per, + // but generally this should be a minimal overhead. + zlcontainer.selectAll('path').sort(function(da, db) { + return axisIds.idSort(da.id, db.id); + }); + }); zl.attr('transform', transfn) .call(Color.stroke, ax.zerolinecolor || Color.defaultLine) .style('stroke-width', zeroLineWidth + 'px'); @@ -2326,54 +2263,59 @@ axes.doTicks = function(gd, axid, skipTitle) { } return drawLabels(ax._axislayer, ax._pos); } - else { + else if(fullLayout._has('cartesian')) { subplots = axes.getSubplots(gd, ax); - var alldone = subplots.map(function(subplot) { + + // keep track of which subplots (by main conteraxis) we've already + // drawn grids for, so we don't overdraw overlaying subplots + var finishedGrids = {}; + + subplots.map(function(subplot) { var plotinfo = fullLayout._plots[subplot]; + var counterAxis = plotinfo[counterLetter + 'axis']; - if(!fullLayout._has('cartesian')) return; + var mainCounterID = counterAxis._mainAxis._id; + if(finishedGrids[mainCounterID]) return; + finishedGrids[mainCounterID] = 1; - var container = plotinfo[axLetter + 'axislayer'], + drawGrid(plotinfo, counterAxis, subplot); + }); - // [bottom or left, top or right, free, main] - linepositions = ax._linepositions[subplot] || [], - counteraxis = plotinfo[counterLetter + 'axis'], - mainSubplot = counteraxis._id === ax.anchor, - ticksides = [false, false, false], - tickpath = ''; + var mainSubplot = ax._mainSubplot; + var mainPlotinfo = fullLayout._plots[mainSubplot]; + var tickSubplots = []; - // ticks - if(ax.mirror === 'allticks') ticksides = [true, true, false]; - else if(mainSubplot) { - if(ax.mirror === 'ticks') ticksides = [true, true, false]; - else ticksides[sides.indexOf(axside)] = true; - } - if(ax.mirrors) { - for(i = 0; i < 2; i++) { - var thisMirror = ax.mirrors[counteraxis._id + sides[i]]; - if(thisMirror === 'ticks' || thisMirror === 'labels') { - ticksides[i] = true; - } - } + if(ax.ticks) { + var mainSign = ticksign[2]; + var tickpath = tickpathfn(ax._mainLinePosition + pad * mainSign, mainSign * ax.ticklen); + if(ax._anchorAxis && ax.mirror && ax.mirror !== true) { + tickpath += tickpathfn(ax._mainMirrorPosition - pad * mainSign, -mainSign * ax.ticklen); } + drawTicks(mainPlotinfo[axLetter + 'axislayer'], tickpath); - // free axis ticks - if(linepositions[2] !== undefined) ticksides[2] = true; + tickSubplots = Object.keys(ax._linepositions); + } - ticksides.forEach(function(showside, sidei) { - var pos = linepositions[sidei], - tsign = ticksign[sidei]; - if(showside && isNumeric(pos)) { - tickpath += tickpathfn(pos + pad * tsign, tsign * ax.ticklen); - } - }); + tickSubplots.map(function(subplot) { + var plotinfo = fullLayout._plots[subplot]; + + var container = plotinfo[axLetter + 'axislayer']; + + // [bottom or left, top or right] + // free and main are handled above + var linepositions = ax._linepositions[subplot] || []; + + function tickPathSide(sidei) { + var tsign = ticksign[sidei]; + return tickpathfn(linepositions[sidei] + pad * tsign, tsign * ax.ticklen); + } + + drawTicks(container, tickPathSide(0) + tickPathSide(1)); + }); - drawTicks(container, tickpath); - drawGrid(plotinfo, counteraxis, subplot); - return drawLabels(container, linepositions[3]); - }).filter(function(onedone) { return onedone && onedone.then; }); + var mainContainer = mainPlotinfo[axLetter + 'axislayer']; - return alldone.length ? Promise.all(alldone) : 0; + return drawLabels(mainContainer, ax._mainLinePosition); } }; diff --git a/src/plots/cartesian/axis_ids.js b/src/plots/cartesian/axis_ids.js index 63b9fae6e77..e53306695f9 100644 --- a/src/plots/cartesian/axis_ids.js +++ b/src/plots/cartesian/axis_ids.js @@ -9,8 +9,6 @@ 'use strict'; var Registry = require('../../registry'); -var Plots = require('../plots'); -var Lib = require('../../lib'); var constants = require('./constants'); @@ -41,53 +39,43 @@ exports.cleanId = function cleanId(id, axLetter) { return id.charAt(0) + axNum; }; -// get all axis object names -// optionally restricted to only x or y or z by string axLetter -// and optionally 2D axes only, not those inside 3D scenes -function listNames(gd, axLetter, only2d) { +// get all axis objects, as restricted in listNames +exports.list = function(gd, axLetter, only2d) { var fullLayout = gd._fullLayout; if(!fullLayout) return []; - function filterAxis(obj, extra) { - var keys = Object.keys(obj), - axMatch = /^[xyz]axis[0-9]*/, - out = []; - - for(var i = 0; i < keys.length; i++) { - var k = keys[i]; - if(axLetter && k.charAt(0) !== axLetter) continue; - if(axMatch.test(k)) out.push(extra + k); - } + var idList = exports.listIds(gd, axLetter); + var out = new Array(idList.length); + var i; - return out.sort(); + for(i = 0; i < idList.length; i++) { + var idi = idList[i]; + out[i] = fullLayout[idi.charAt(0) + 'axis' + idi.substr(1)]; } - var names = filterAxis(fullLayout, ''); - if(only2d) return names; + if(!only2d) { + var sceneIds3D = fullLayout._subplots.gl3d || []; - var sceneIds3D = Plots.getSubplotIds(fullLayout, 'gl3d') || []; - for(var i = 0; i < sceneIds3D.length; i++) { - var sceneId = sceneIds3D[i]; - names = names.concat( - filterAxis(fullLayout[sceneId], sceneId + '.') - ); - } + for(i = 0; i < sceneIds3D.length; i++) { + var scene = fullLayout[sceneIds3D[i]]; - return names; -} + if(axLetter) out.push(scene[axLetter + 'axis']); + else out.push(scene.xaxis, scene.yaxis, scene.zaxis); + } + } -// get all axis objects, as restricted in listNames -exports.list = function(gd, axletter, only2d) { - return listNames(gd, axletter, only2d) - .map(function(axName) { - return Lib.nestedProperty(gd._fullLayout, axName).get(); - }); + return out; }; // get all axis ids, optionally restricted by letter // this only makes sense for 2d axes -exports.listIds = function(gd, axletter) { - return listNames(gd, axletter, true).map(exports.name2id); +exports.listIds = function(gd, axLetter) { + var fullLayout = gd._fullLayout; + if(!fullLayout) return []; + + var subplotLists = fullLayout._subplots; + if(axLetter) return subplotLists[axLetter + 'axis']; + return subplotLists.xaxis.concat(subplotLists.yaxis); }; // get an axis object from its id 'x','x2' etc @@ -118,3 +106,11 @@ exports.getFromTrace = function(gd, fullTrace, type) { return ax; }; + +// sort x, x2, x10, y, y2, y10... +exports.idSort = function(id1, id2) { + var letter1 = id1.charAt(0); + var letter2 = id2.charAt(0); + if(letter1 !== letter2) return letter1 > letter2 ? 1 : -1; + return +(id1.substr(1) || 1) - +(id2.substr(1) || 1); +}; diff --git a/src/plots/cartesian/constants.js b/src/plots/cartesian/constants.js index 6edfe4fcb9f..c07d83d3199 100644 --- a/src/plots/cartesian/constants.js +++ b/src/plots/cartesian/constants.js @@ -29,6 +29,9 @@ module.exports = { AX_ID_PATTERN: /^[xyz][0-9]*$/, AX_NAME_PATTERN: /^[xyz]axis[0-9]*$/, + // and for 2D subplots + SUBPLOT_PATTERN: /^x([0-9]*)y([0-9]*)$/, + // pixels to move mouse before you stop clamping to starting point MINDRAG: 8, diff --git a/src/plots/cartesian/constraints.js b/src/plots/cartesian/constraints.js index ac5cca0b979..bbcef1e4a52 100644 --- a/src/plots/cartesian/constraints.js +++ b/src/plots/cartesian/constraints.js @@ -19,7 +19,7 @@ var FROM_BL = require('../../constants/alignment').FROM_BL; exports.enforce = function enforceAxisConstraints(gd) { var fullLayout = gd._fullLayout; - var constraintGroups = fullLayout._axisConstraintGroups; + var constraintGroups = fullLayout._axisConstraintGroups || []; var i, j, axisID, ax, normScale, mode, factor; diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 572fdc6c5e8..e76827ca92f 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -9,8 +9,6 @@ 'use strict'; -var isNumeric = require('fast-isnumeric'); - var Fx = require('../../components/fx'); var dragElement = require('../../components/dragelement'); @@ -38,25 +36,17 @@ module.exports = function initInteractions(gd) { subplots.forEach(function(subplot) { var plotinfo = fullLayout._plots[subplot]; - var xa = plotinfo.xaxis, - ya = plotinfo.yaxis, - - // the y position of the main x axis line - y0 = (xa._linepositions[subplot] || [])[3], - - // the x position of the main y axis line - x0 = (ya._linepositions[subplot] || [])[3]; + var xa = plotinfo.xaxis; + var ya = plotinfo.yaxis; var DRAGGERSIZE = constants.DRAGGERSIZE; - if(isNumeric(y0) && xa.side === 'top') y0 -= DRAGGERSIZE; - if(isNumeric(x0) && ya.side !== 'right') x0 -= DRAGGERSIZE; // main and corner draggers need not be repeated for // overlaid subplots - these draggers drag them all if(!plotinfo.mainplot) { // main dragger goes over the grids and data, so we use its // mousemove events for all data hover effects - var maindrag = dragBox(gd, plotinfo, 0, 0, + var maindrag = dragBox(gd, plotinfo, xa._offset, ya._offset, xa._length, ya._length, 'ns', 'ew'); maindrag.onmousemove = function(evt) { @@ -100,36 +90,40 @@ module.exports = function initInteractions(gd) { // corner draggers if(gd._context.showAxisDragHandles) { - dragBox(gd, plotinfo, -DRAGGERSIZE, -DRAGGERSIZE, + dragBox(gd, plotinfo, xa._offset - DRAGGERSIZE, ya._offset - DRAGGERSIZE, DRAGGERSIZE, DRAGGERSIZE, 'n', 'w'); - dragBox(gd, plotinfo, xa._length, -DRAGGERSIZE, + dragBox(gd, plotinfo, xa._offset + xa._length, ya._offset - DRAGGERSIZE, DRAGGERSIZE, DRAGGERSIZE, 'n', 'e'); - dragBox(gd, plotinfo, -DRAGGERSIZE, ya._length, + dragBox(gd, plotinfo, xa._offset - DRAGGERSIZE, ya._offset + ya._length, DRAGGERSIZE, DRAGGERSIZE, 's', 'w'); - dragBox(gd, plotinfo, xa._length, ya._length, + dragBox(gd, plotinfo, xa._offset + xa._length, ya._offset + ya._length, DRAGGERSIZE, DRAGGERSIZE, 's', 'e'); } } if(gd._context.showAxisDragHandles) { // x axis draggers - if you have overlaid plots, // these drag each axis separately - if(isNumeric(y0)) { - if(xa.anchor === 'free') y0 -= fullLayout._size.h * (1 - ya.domain[1]); - dragBox(gd, plotinfo, xa._length * 0.1, y0, + if(subplot === xa._mainSubplot) { + // the y position of the main x axis line + var y0 = xa._mainLinePosition; + if(xa.side === 'top') y0 -= DRAGGERSIZE; + dragBox(gd, plotinfo, xa._offset + xa._length * 0.1, y0, xa._length * 0.8, DRAGGERSIZE, '', 'ew'); - dragBox(gd, plotinfo, 0, y0, + dragBox(gd, plotinfo, xa._offset, y0, xa._length * 0.1, DRAGGERSIZE, '', 'w'); - dragBox(gd, plotinfo, xa._length * 0.9, y0, + dragBox(gd, plotinfo, xa._offset + xa._length * 0.9, y0, xa._length * 0.1, DRAGGERSIZE, '', 'e'); } // y axis draggers - if(isNumeric(x0)) { - if(ya.anchor === 'free') x0 -= fullLayout._size.w * xa.domain[0]; - dragBox(gd, plotinfo, x0, ya._length * 0.1, + if(subplot === ya._mainSubplot) { + // the x position of the main y axis line + var x0 = ya._mainLinePosition; + if(ya.side !== 'right') x0 -= DRAGGERSIZE; + dragBox(gd, plotinfo, x0, ya._offset + ya._length * 0.1, DRAGGERSIZE, ya._length * 0.8, 'ns', ''); - dragBox(gd, plotinfo, x0, ya._length * 0.9, + dragBox(gd, plotinfo, x0, ya._offset + ya._length * 0.9, DRAGGERSIZE, ya._length * 0.1, 's', ''); - dragBox(gd, plotinfo, x0, 0, + dragBox(gd, plotinfo, x0, ya._offset, DRAGGERSIZE, ya._length * 0.1, 'n', ''); } } diff --git a/src/plots/cartesian/include_components.js b/src/plots/cartesian/include_components.js new file mode 100644 index 00000000000..3a2ca4097b5 --- /dev/null +++ b/src/plots/cartesian/include_components.js @@ -0,0 +1,73 @@ +/** +* Copyright 2012-2017, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var Registry = require('../../registry'); +var Lib = require('../../lib'); + +/** + * Factory function for checking component arrays for subplot references. + * + * @param {string} containerArrayName: the top-level array in gd.layout to check + * If an item in this container is found that references a cartesian x and/or y axis, + * ensure cartesian is marked as a base plot module and record the axes (and subplot + * if both refs are axes) in gd._fullLayout + * + * @return {function}: with args layoutIn (gd.layout) and layoutOut (gd._fullLayout) + * as expected of a component includeBasePlot method + */ +module.exports = function makeIncludeComponents(containerArrayName) { + return function includeComponents(layoutIn, layoutOut) { + var array = layoutIn[containerArrayName]; + if(!Array.isArray(array)) return; + + var Cartesian = Registry.subplotsRegistry.cartesian; + var idRegex = Cartesian.idRegex; + var subplots = layoutOut._subplots; + var xaList = subplots.xaxis; + var yaList = subplots.yaxis; + var cartesianList = subplots.cartesian; + var hasCartesianOrGL2D = layoutOut._has('cartesian') || layoutOut._has('gl2d'); + + for(var i = 0; i < array.length; i++) { + var itemi = array[i]; + if(!Lib.isPlainObject(itemi)) continue; + + var xref = itemi.xref; + var yref = itemi.yref; + + var hasXref = idRegex.x.test(xref); + var hasYref = idRegex.y.test(yref); + if(hasXref || hasYref) { + if(!hasCartesianOrGL2D) Lib.pushUnique(layoutOut._basePlotModules, Cartesian); + + var newAxis = false; + if(hasXref && xaList.indexOf(xref) === -1) { + xaList.push(xref); + newAxis = true; + } + if(hasYref && yaList.indexOf(yref) === -1) { + yaList.push(yref); + newAxis = true; + } + + /* + * Notice the logic here: only add a subplot for a component if + * it's referencing both x and y axes AND it's creating a new axis + * so for example if your plot already has xy and x2y2, an annotation + * on x2y or xy2 will not create a new subplot. + */ + if(newAxis && hasXref && hasYref) { + cartesianList.push(xref + yref); + } + } + } + }; +}; diff --git a/src/plots/cartesian/index.js b/src/plots/cartesian/index.js index a9db08dd172..4b83662d6d9 100644 --- a/src/plots/cartesian/index.js +++ b/src/plots/cartesian/index.js @@ -12,6 +12,7 @@ var d3 = require('d3'); var Lib = require('../../lib'); var Plots = require('../plots'); +var getModuleCalcData = require('../get_data').getModuleCalcData; var axisIds = require('./axis_ids'); var constants = require('./constants'); @@ -30,13 +31,92 @@ exports.attributes = require('./attributes'); exports.layoutAttributes = require('./layout_attributes'); +exports.supplyLayoutDefaults = require('./layout_defaults'); + exports.transitionAxes = require('./transition_axes'); +exports.finalizeSubplots = function(layoutIn, layoutOut) { + var subplots = layoutOut._subplots; + var xList = subplots.xaxis; + var yList = subplots.yaxis; + var spSVG = subplots.cartesian; + var spAll = spSVG.concat(subplots.gl2d || []); + var allX = {}; + var allY = {}; + var i, xi, yi; + + for(i = 0; i < spAll.length; i++) { + var parts = spAll[i].split('y'); + allX[parts[0]] = 1; + allY['y' + parts[1]] = 1; + } + + // check for x axes with no subplot, and make one from the anchor of that x axis + for(i = 0; i < xList.length; i++) { + xi = xList[i]; + if(!allX[xi]) { + yi = (layoutIn[axisIds.id2name(xi)] || {}).anchor; + if(!constants.idRegex.y.test(yi)) yi = 'y'; + spSVG.push(xi + yi); + spAll.push(xi + yi); + + if(!allY[yi]) { + allY[yi] = 1; + Lib.pushUnique(yList, yi); + } + } + } + + // same for y axes with no subplot + for(i = 0; i < yList.length; i++) { + yi = yList[i]; + if(!allY[yi]) { + xi = (layoutIn[axisIds.id2name(yi)] || {}).anchor; + if(!constants.idRegex.x.test(xi)) xi = 'x'; + spSVG.push(xi + yi); + spAll.push(xi + yi); + + if(!allX[xi]) { + allX[xi] = 1; + Lib.pushUnique(xList, xi); + } + } + } + + // finally, if we've gotten here we're supposed to show cartesian... + // so if there are NO subplots at all, make one from the first + // x & y axes in the input layout + if(!spAll.length) { + var keys = Object.keys(layoutIn); + xi = ''; + yi = ''; + for(i = 0; i < keys.length; i++) { + var ki = keys[i]; + if(constants.attrRegex.test(ki)) { + var axLetter = ki.charAt(0); + if(axLetter === 'x') { + if(!xi || (+ki.substr(5) < +xi.substr(5))) { + xi = ki; + } + } + else if(!yi || (+ki.substr(5) < +yi.substr(5))) { + yi = ki; + } + } + } + xi = xi ? axisIds.name2id(xi) : 'x'; + yi = yi ? axisIds.name2id(yi) : 'y'; + xList.push(xi); + yList.push(yi); + spSVG.push(xi + yi); + } +}; + exports.plot = function(gd, traces, transitionOpts, makeOnCompleteCallback) { - var fullLayout = gd._fullLayout, - subplots = Plots.getSubplotIds(fullLayout, 'cartesian'), - calcdata = gd.calcdata, - i; + var fullLayout = gd._fullLayout; + var subplots = fullLayout._subplots.cartesian; + var calcdata = gd.calcdata; + var i; // If traces is not provided, then it's a complete replot and missing // traces are removed @@ -127,15 +207,7 @@ function plotOne(gd, plotinfo, cdSubplot, transitionOpts, makeOnCompleteCallback if(_module.basePlotModule.name !== 'cartesian') continue; // plot all traces of this type on this subplot at once - var cdModule = []; - for(var k = 0; k < cdSubplot.length; k++) { - var cd = cdSubplot[k], - trace = cd[0].trace; - - if((trace._module === _module) && (trace.visible === true)) { - cdModule.push(cd); - } - } + var cdModule = getModuleCalcData(cdSubplot, _module); _module.plot(gd, plotinfo, cdModule, transitionOpts, makeOnCompleteCallback); } @@ -181,19 +253,29 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) .remove(); } + var oldSubplotList = oldFullLayout._subplots || {}; + var newSubplotList = newFullLayout._subplots || {xaxis: [], yaxis: []}; + + // delete any titles we don't need anymore + // check if axis list has changed, and if so clear old titles + if(oldSubplotList.xaxis && oldSubplotList.yaxis) { + var oldAxIDs = oldSubplotList.xaxis.concat(oldSubplotList.yaxis); + var newAxIDs = newSubplotList.xaxis.concat(newSubplotList.yaxis); + + for(i = 0; i < oldAxIDs.length; i++) { + if(newAxIDs.indexOf(oldAxIDs[i]) === -1) { + oldFullLayout._infolayer.selectAll('.g-' + oldAxIDs[i] + 'title').remove(); + } + } + } + + // if we've gotten rid of all cartesian traces, remove all the subplot svg items var hadCartesian = (oldFullLayout._has && oldFullLayout._has('cartesian')); var hasCartesian = (newFullLayout._has && newFullLayout._has('cartesian')); if(hadCartesian && !hasCartesian) { - var subplotLayers = oldFullLayout._cartesianlayer.selectAll('.subplot'); - var axIds = axisIds.listIds({ _fullLayout: oldFullLayout }); - - subplotLayers.call(purgeSubplotLayers, oldFullLayout); + purgeSubplotLayers(oldFullLayout._cartesianlayer.selectAll('.subplot'), oldFullLayout); oldFullLayout._defs.selectAll('.axesclip').remove(); - - for(i = 0; i < axIds.length; i++) { - oldFullLayout._infolayer.select('.' + axIds[i] + 'title').remove(); - } } }; @@ -287,10 +369,8 @@ function makeSubplotLayer(plotinfo) { plotinfo.imagelayer = joinLayer(backLayer, 'g', 'imagelayer'); plotinfo.gridlayer = joinLayer(plotgroup, 'g', 'gridlayer'); - plotinfo.overgrid = joinLayer(plotgroup, 'g', 'overgrid'); plotinfo.zerolinelayer = joinLayer(plotgroup, 'g', 'zerolinelayer'); - plotinfo.overzero = joinLayer(plotgroup, 'g', 'overzero'); joinLayer(plotgroup, 'path', 'xlines-below'); joinLayer(plotgroup, 'path', 'ylines-below'); @@ -328,8 +408,8 @@ function makeSubplotLayer(plotinfo) { // their other components to the corresponding // extra groups of their main plots. - plotinfo.gridlayer = joinLayer(mainplotinfo.overgrid, 'g', id); - plotinfo.zerolinelayer = joinLayer(mainplotinfo.overzero, 'g', id); + plotinfo.gridlayer = mainplotinfo.gridlayer; + plotinfo.zerolinelayer = mainplotinfo.zerolinelayer; joinLayer(mainplotinfo.overlinesBelow, 'path', xId); joinLayer(mainplotinfo.overlinesBelow, 'path', yId); @@ -350,6 +430,10 @@ function makeSubplotLayer(plotinfo) { plotinfo.yaxislayer = mainplotgroup.select('.overaxes-' + yLayer).select('.' + yId); } + joinLayer(plotinfo.gridlayer, 'g', plotinfo.xaxis._id, plotinfo.xaxis._id); + joinLayer(plotinfo.gridlayer, 'g', plotinfo.yaxis._id, plotinfo.yaxis._id); + plotinfo.gridlayer.selectAll('g').sort(axisIds.idSort); + // common attributes for all subplots, overlays or not for(var i = 0; i < constants.traceLayerClasses.length; i++) { @@ -403,9 +487,9 @@ function purgeSubplotLayers(layers, fullLayout) { } } -function joinLayer(parent, nodeType, className) { +function joinLayer(parent, nodeType, className, dataVal) { var layer = parent.selectAll('.' + className) - .data([0]); + .data([dataVal || 0]); layer.enter().append(nodeType) .classed(className, true); diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index 6765fdcdec7..c4f5ef57c05 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -14,7 +14,6 @@ var Lib = require('../../lib'); var Color = require('../../components/color'); var basePlotLayoutAttributes = require('../layout_attributes'); -var constants = require('./constants'); var layoutAttributes = require('./layout_attributes'); var handleTypeDefaults = require('./type_defaults'); var handleAxisDefaults = require('./axis_defaults'); @@ -24,40 +23,28 @@ var axisIds = require('./axis_ids'); module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { - var layoutKeys = Object.keys(layoutIn), - xaListCartesian = [], - yaListCartesian = [], - xaListGl2d = [], - yaListGl2d = [], - xaListCheater = [], - xaListNonCheater = [], - outerTicks = {}, - noGrids = {}, - i; + var xaCheater = {}; + var xaNonCheater = {}; + var outerTicks = {}; + var noGrids = {}; + var i; // look for axes in the data for(i = 0; i < fullData.length; i++) { var trace = fullData[i]; - var listX, listY; - if(Registry.traceIs(trace, 'cartesian')) { - listX = xaListCartesian; - listY = yaListCartesian; + if(!Registry.traceIs(trace, 'cartesian') && !Registry.traceIs(trace, 'gl2d')) { + continue; } - else if(Registry.traceIs(trace, 'gl2d')) { - listX = xaListGl2d; - listY = yaListGl2d; - } - else continue; - var xaName = axisIds.id2name(trace.xaxis), - yaName = axisIds.id2name(trace.yaxis); + var xaName = axisIds.id2name(trace.xaxis); + var yaName = axisIds.id2name(trace.yaxis); // Two things trigger axis visibility: // 1. is not carpet // 2. carpet that's not cheater if(!Registry.traceIs(trace, 'carpet') || (trace.type === 'carpet' && !trace._cheater)) { - if(xaName) Lib.pushUnique(xaListNonCheater, xaName); + if(xaName) xaNonCheater[xaName] = 1; } // The above check for definitely-not-cheater is not adequate. This @@ -65,13 +52,9 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { // full condition triggering hiding is: // *could* be a cheater and *is not definitely visible* if(trace.type === 'carpet' && trace._cheater) { - if(xaName) Lib.pushUnique(xaListCheater, xaName); + if(xaName) xaCheater[xaName] = 1; } - // add axes implied by traces - if(xaName && listX.indexOf(xaName) === -1) listX.push(xaName); - if(yaName && listY.indexOf(yaName) === -1) listY.push(yaName); - // check for default formatting tweaks if(Registry.traceIs(trace, '2dMap')) { outerTicks[xaName] = true; @@ -84,51 +67,17 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { } } - // N.B. Ignore orphan axes (i.e. axes that have no data attached to them) - // if gl3d or geo is present on graph. This is retain backward compatible. - // - // TODO drop this in version 2.0 - var ignoreOrphan = (layoutOut._has('gl3d') || layoutOut._has('geo')); - - if(!ignoreOrphan) { - for(i = 0; i < layoutKeys.length; i++) { - var key = layoutKeys[i]; - - // orphan layout axes are considered cartesian subplots - - if(xaListGl2d.indexOf(key) === -1 && - xaListCartesian.indexOf(key) === -1 && - constants.xAxisMatch.test(key)) { - xaListCartesian.push(key); - } - else if(yaListGl2d.indexOf(key) === -1 && - yaListCartesian.indexOf(key) === -1 && - constants.yAxisMatch.test(key)) { - yaListCartesian.push(key); - } - } - } - - // make sure that plots with orphan cartesian axes - // are considered 'cartesian' - if(xaListCartesian.length && yaListCartesian.length) { - Lib.pushUnique(layoutOut._basePlotModules, Registry.subplotsRegistry.cartesian); - } - - function axSort(a, b) { - var aNum = Number(a.substr(5) || 1), - bNum = Number(b.substr(5) || 1); - return aNum - bNum; - } - - var xaList = xaListCartesian.concat(xaListGl2d).sort(axSort), - yaList = yaListCartesian.concat(yaListGl2d).sort(axSort), - axesList = xaList.concat(yaList); + var subplots = layoutOut._subplots; + var xIds = subplots.xaxis; + var yIds = subplots.yaxis; + var xNames = Lib.simpleMap(xIds, axisIds.id2name); + var yNames = Lib.simpleMap(yIds, axisIds.id2name); + var axNames = xNames.concat(yNames); // plot_bgcolor only makes sense if there's a (2D) plot! // TODO: bgcolor for each subplot, to inherit from the main one var plot_bgcolor = Color.background; - if(xaList.length && yaList.length) { + if(xIds.length && yIds.length) { plot_bgcolor = Lib.coerce(layoutIn, layoutOut, basePlotLayoutAttributes, 'plot_bgcolor'); } @@ -141,14 +90,13 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { } function getCounterAxes(axLetter) { - var list = {x: yaList, y: xaList}[axLetter]; - return Lib.simpleMap(list, axisIds.name2id); + return (axLetter === 'x') ? yIds : xIds; } var counterAxes = {x: getCounterAxes('x'), y: getCounterAxes('y')}; function getOverlayableAxes(axLetter, axName) { - var list = {x: xaList, y: yaList}[axLetter]; + var list = (axLetter === 'x') ? xNames : yNames; var out = []; for(var j = 0; j < list.length; j++) { @@ -163,8 +111,8 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { } // first pass creates the containers, determines types, and handles most of the settings - for(i = 0; i < axesList.length; i++) { - axName = axesList[i]; + for(i = 0; i < axNames.length; i++) { + axName = axNames[i]; if(!Lib.isPlainObject(layoutIn[axName])) { layoutIn[axName] = {}; @@ -186,7 +134,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { data: fullData, bgColor: bgColor, calendar: layoutOut.calendar, - cheateronly: axLetter === 'x' && (xaListCheater.indexOf(axName) !== -1 && xaListNonCheater.indexOf(axName) === -1) + cheateronly: axLetter === 'x' && xaCheater[axName] && !xaNonCheater[axName] }; handleAxisDefaults(axLayoutIn, axLayoutOut, coerce, defaultOptions, layoutOut); @@ -211,11 +159,11 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { } // quick second pass for range slider and selector defaults - var rangeSliderDefaults = Registry.getComponentMethod('rangeslider', 'handleDefaults'), - rangeSelectorDefaults = Registry.getComponentMethod('rangeselector', 'handleDefaults'); + var rangeSliderDefaults = Registry.getComponentMethod('rangeslider', 'handleDefaults'); + var rangeSelectorDefaults = Registry.getComponentMethod('rangeselector', 'handleDefaults'); - for(i = 0; i < xaList.length; i++) { - axName = xaList[i]; + for(i = 0; i < xNames.length; i++) { + axName = xNames[i]; axLayoutIn = layoutIn[axName]; axLayoutOut = layoutOut[axName]; @@ -226,7 +174,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { axLayoutIn, axLayoutOut, layoutOut, - yaList, + yNames, axLayoutOut.calendar ); } @@ -234,8 +182,8 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { coerce('fixedrange'); } - for(i = 0; i < yaList.length; i++) { - axName = yaList[i]; + for(i = 0; i < yNames.length; i++) { + axName = yNames[i]; axLayoutIn = layoutIn[axName]; axLayoutOut = layoutOut[axName]; @@ -259,8 +207,8 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { layoutOut._axisConstraintGroups = []; var allAxisIds = counterAxes.x.concat(counterAxes.y); - for(i = 0; i < axesList.length; i++) { - axName = axesList[i]; + for(i = 0; i < axNames.length; i++) { + axName = axNames[i]; axLetter = axName.charAt(0); axLayoutIn = layoutIn[axName]; diff --git a/src/plots/geo/index.js b/src/plots/geo/index.js index 076411c0d82..388136794ab 100644 --- a/src/plots/geo/index.js +++ b/src/plots/geo/index.js @@ -10,7 +10,7 @@ 'use strict'; var createGeo = require('./geo'); -var Plots = require('../../plots/plots'); +var getSubplotCalcData = require('../../plots/get_data').getSubplotCalcData; var counterRegex = require('../../lib').counterRegex; var GEO = 'geo'; @@ -32,7 +32,7 @@ exports.supplyLayoutDefaults = require('./layout/defaults'); exports.plot = function plotGeo(gd) { var fullLayout = gd._fullLayout; var calcData = gd.calcdata; - var geoIds = Plots.getSubplotIds(fullLayout, GEO); + var geoIds = fullLayout._subplots[GEO]; /** * If 'plotly-geo-assets.js' is not included, @@ -44,7 +44,7 @@ exports.plot = function plotGeo(gd) { for(var i = 0; i < geoIds.length; i++) { var geoId = geoIds[i]; - var geoCalcData = Plots.getSubplotCalcData(calcData, GEO, geoId); + var geoCalcData = getSubplotCalcData(calcData, GEO, geoId); var geoLayout = fullLayout[geoId]; var geo = geoLayout._subplot; @@ -65,7 +65,7 @@ exports.plot = function plotGeo(gd) { }; exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { - var oldGeoKeys = Plots.getSubplotIds(oldFullLayout, GEO); + var oldGeoKeys = oldFullLayout._subplots[GEO] || []; for(var i = 0; i < oldGeoKeys.length; i++) { var oldGeoKey = oldGeoKeys[i]; @@ -79,7 +79,7 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) }; exports.updateFx = function(fullLayout) { - var subplotIds = Plots.getSubplotIds(fullLayout, GEO); + var subplotIds = fullLayout._subplots[GEO]; for(var i = 0; i < subplotIds.length; i++) { var subplotLayout = fullLayout[subplotIds[i]]; diff --git a/src/plots/get_data.js b/src/plots/get_data.js new file mode 100644 index 00000000000..765388964d7 --- /dev/null +++ b/src/plots/get_data.js @@ -0,0 +1,92 @@ +/** +* Copyright 2012-2017, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var Registry = require('../registry'); +var SUBPLOT_PATTERN = require('./cartesian/constants').SUBPLOT_PATTERN; + +/** + * Get calcdata traces(s) associated with a given subplot + * + * @param {array} calcData (as in gd.calcdata) + * @param {string} type subplot type + * @param {string} subplotId subplot id to look for + * + * @return {array} array of calcdata traces + */ +exports.getSubplotCalcData = function(calcData, type, subplotId) { + var basePlotModule = Registry.subplotsRegistry[type]; + if(!basePlotModule) return []; + + var attr = basePlotModule.attr; + var subplotCalcData = []; + + for(var i = 0; i < calcData.length; i++) { + var calcTrace = calcData[i]; + var trace = calcTrace[0].trace; + + if(trace[attr] === subplotId) subplotCalcData.push(calcTrace); + } + + return subplotCalcData; +}; + +exports.getModuleCalcData = function(calcdata, typeOrModule) { + var moduleCalcData = []; + var _module = typeof typeOrModule === 'string' ? Registry.getModule(typeOrModule) : typeOrModule; + if(!_module) return moduleCalcData; + + for(var i = 0; i < calcdata.length; i++) { + var cd = calcdata[i]; + var trace = cd[0].trace; + + if((trace._module === _module) && (trace.visible === true)) moduleCalcData.push(cd); + } + + return moduleCalcData; +}; + +/** + * Get the data trace(s) associated with a given subplot. + * + * @param {array} data plotly full data array. + * @param {string} type subplot type to look for. + * @param {string} subplotId subplot id to look for. + * + * @return {array} list of trace objects. + * + */ +exports.getSubplotData = function getSubplotData(data, type, subplotId) { + if(!Registry.subplotsRegistry[type]) return []; + + var attr = Registry.subplotsRegistry[type].attr; + var subplotData = []; + var trace, subplotX, subplotY; + + if(type === 'gl2d') { + var spmatch = subplotId.match(SUBPLOT_PATTERN); + subplotX = 'x' + spmatch[1]; + subplotY = 'y' + spmatch[2]; + } + + for(var i = 0; i < data.length; i++) { + trace = data[i]; + + if(type === 'gl2d' && Registry.traceIs(trace, 'gl2d')) { + if(trace[attr[0]] === subplotX && trace[attr[1]] === subplotY) { + subplotData.push(trace); + } + } + else { + if(trace[attr] === subplotId) subplotData.push(trace); + } + } + + return subplotData; +}; diff --git a/src/plots/gl2d/convert.js b/src/plots/gl2d/convert.js index fd50b3a787d..948fa8e4d47 100644 --- a/src/plots/gl2d/convert.js +++ b/src/plots/gl2d/convert.js @@ -9,7 +9,6 @@ 'use strict'; -var Plots = require('../plots'); var Axes = require('../cartesian/axes'); var convertHTMLToUnicode = require('../../lib/html2unicode'); @@ -183,9 +182,9 @@ proto.merge = function(options) { // is an axis shared with an already-drawn subplot ? proto.hasSharedAxis = function(ax) { - var scene = this.scene, - subplotIds = Plots.getSubplotIds(scene.fullLayout, 'gl2d'), - list = Axes.findSubplotsWithAxis(subplotIds, ax); + var scene = this.scene; + var subplotIds = scene.fullLayout._subplots.gl2d; + var list = Axes.findSubplotsWithAxis(subplotIds, ax); // if index === 0, then the subplot is already drawn as subplots // are drawn in order. diff --git a/src/plots/gl2d/index.js b/src/plots/gl2d/index.js index 177de42ffab..eba58d8442b 100644 --- a/src/plots/gl2d/index.js +++ b/src/plots/gl2d/index.js @@ -12,11 +12,12 @@ var overrideAll = require('../../plot_api/edit_types').overrideAll; var Scene2D = require('./scene2d'); -var Plots = require('../plots'); +var layoutGlobalAttrs = require('../layout_attributes'); var xmlnsNamespaces = require('../../constants/xmlns_namespaces'); var constants = require('../cartesian/constants'); var Cartesian = require('../cartesian'); var fxAttrs = require('../../components/fx/layout_attributes'); +var getSubplotData = require('../get_data').getSubplotData; exports.name = 'gl2d'; @@ -30,6 +31,12 @@ exports.attrRegex = constants.attrRegex; exports.attributes = require('../cartesian/attributes'); +exports.supplyLayoutDefaults = function(layoutIn, layoutOut, fullData) { + if(!layoutOut._has('cartesian')) { + Cartesian.supplyLayoutDefaults(layoutIn, layoutOut, fullData); + } +}; + // gl2d uses svg axis attributes verbatim, but overrides editType // this could potentially be just `layoutAttributes` but it would // still need special handling somewhere to give it precedence over @@ -38,7 +45,7 @@ exports.layoutAttrOverrides = overrideAll(Cartesian.layoutAttributes, 'plot', 'f // similar overrides for base plot attributes (and those added by components) exports.baseLayoutAttrOverrides = overrideAll({ - plot_bgcolor: Plots.layoutAttributes.plot_bgcolor, + plot_bgcolor: layoutGlobalAttrs.plot_bgcolor, hoverlabel: fxAttrs.hoverlabel // dragmode needs calc but only when transitioning TO lasso or select // so for now it's left inside _relayout @@ -46,14 +53,14 @@ exports.baseLayoutAttrOverrides = overrideAll({ }, 'plot', 'nested'); exports.plot = function plotGl2d(gd) { - var fullLayout = gd._fullLayout, - fullData = gd._fullData, - subplotIds = Plots.getSubplotIds(fullLayout, 'gl2d'); + var fullLayout = gd._fullLayout; + var fullData = gd._fullData; + var subplotIds = fullLayout._subplots.gl2d; for(var i = 0; i < subplotIds.length; i++) { var subplotId = subplotIds[i], subplotObj = fullLayout._plots[subplotId], - fullSubplotData = Plots.getSubplotData(fullData, 'gl2d', subplotId); + fullSubplotData = getSubplotData(fullData, 'gl2d', subplotId); // ref. to corresp. Scene instance var scene = subplotObj._scene2d; @@ -79,7 +86,7 @@ exports.plot = function plotGl2d(gd) { }; exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { - var oldSceneKeys = Plots.getSubplotIds(oldFullLayout, 'gl2d'); + var oldSceneKeys = oldFullLayout._subplots.gl2d || []; for(var i = 0; i < oldSceneKeys.length; i++) { var id = oldSceneKeys[i], @@ -89,7 +96,7 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) if(!oldSubplot._scene2d) continue; // if no traces are present, delete gl2d subplot - var subplotData = Plots.getSubplotData(newFullData, 'gl2d', id); + var subplotData = getSubplotData(newFullData, 'gl2d', id); if(subplotData.length === 0) { oldSubplot._scene2d.destroy(); delete oldFullLayout._plots[id]; @@ -107,8 +114,8 @@ exports.drawFramework = function(gd) { }; exports.toSVG = function(gd) { - var fullLayout = gd._fullLayout, - subplotIds = Plots.getSubplotIds(fullLayout, 'gl2d'); + var fullLayout = gd._fullLayout; + var subplotIds = fullLayout._subplots.gl2d; for(var i = 0; i < subplotIds.length; i++) { var subplot = fullLayout._plots[subplotIds[i]], @@ -132,7 +139,7 @@ exports.toSVG = function(gd) { }; exports.updateFx = function(fullLayout) { - var subplotIds = Plots.getSubplotIds(fullLayout, 'gl2d'); + var subplotIds = fullLayout._subplots.gl2d; for(var i = 0; i < subplotIds.length; i++) { var subplotObj = fullLayout._plots[subplotIds[i]]._scene2d; diff --git a/src/plots/gl2d/scene2d.js b/src/plots/gl2d/scene2d.js index 2d8b3a74ffb..3a6433d6dc8 100644 --- a/src/plots/gl2d/scene2d.js +++ b/src/plots/gl2d/scene2d.js @@ -22,13 +22,15 @@ var createOptions = require('./convert'); var createCamera = require('./camera'); var convertHTMLToUnicode = require('../../lib/html2unicode'); var showNoWebGlMsg = require('../../lib/show_no_webgl_msg'); -var axisConstraints = require('../../plots/cartesian/constraints'); +var axisConstraints = require('../cartesian/constraints'); var enforceAxisConstraints = axisConstraints.enforce; var cleanAxisConstraints = axisConstraints.clean; var AXES = ['xaxis', 'yaxis']; var STATIC_CANVAS, STATIC_CONTEXT; +var SUBPLOT_PATTERN = require('../cartesian/constants').SUBPLOT_PATTERN; + function Scene2D(options, fullLayout) { this.container = options.container; @@ -296,9 +298,9 @@ function compareTicks(a, b) { proto.updateRefs = function(newFullLayout) { this.fullLayout = newFullLayout; - var spmatch = Axes.subplotMatch, - xaxisName = 'xaxis' + this.id.match(spmatch)[1], - yaxisName = 'yaxis' + this.id.match(spmatch)[2]; + var spmatch = this.id.match(SUBPLOT_PATTERN); + var xaxisName = 'xaxis' + spmatch[1]; + var yaxisName = 'yaxis' + spmatch[2]; this.xaxis = this.fullLayout[xaxisName]; this.yaxis = this.fullLayout[yaxisName]; diff --git a/src/plots/gl3d/index.js b/src/plots/gl3d/index.js index e3e2e9094ca..60175470f1c 100644 --- a/src/plots/gl3d/index.js +++ b/src/plots/gl3d/index.js @@ -13,7 +13,7 @@ var overrideAll = require('../../plot_api/edit_types').overrideAll; var fxAttrs = require('../../components/fx/layout_attributes'); var Scene = require('./scene'); -var Plots = require('../plots'); +var getSubplotData = require('../get_data').getSubplotData; var Lib = require('../../lib'); var xmlnsNamespaces = require('../../constants/xmlns_namespaces'); @@ -40,13 +40,13 @@ exports.baseLayoutAttrOverrides = overrideAll({ exports.supplyLayoutDefaults = require('./layout/defaults'); exports.plot = function plotGl3d(gd) { - var fullLayout = gd._fullLayout, - fullData = gd._fullData, - sceneIds = Plots.getSubplotIds(fullLayout, GL3D); + var fullLayout = gd._fullLayout; + var fullData = gd._fullData; + var sceneIds = fullLayout._subplots[GL3D]; for(var i = 0; i < sceneIds.length; i++) { var sceneId = sceneIds[i], - fullSceneData = Plots.getSubplotData(fullData, GL3D, sceneId), + fullSceneData = getSubplotData(fullData, GL3D, sceneId), sceneLayout = fullLayout[sceneId], scene = sceneLayout._scene; @@ -75,7 +75,7 @@ exports.plot = function plotGl3d(gd) { }; exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { - var oldSceneKeys = Plots.getSubplotIds(oldFullLayout, GL3D); + var oldSceneKeys = oldFullLayout._subplots[GL3D] || []; for(var i = 0; i < oldSceneKeys.length; i++) { var oldSceneKey = oldSceneKeys[i]; @@ -93,14 +93,14 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) }; exports.toSVG = function(gd) { - var fullLayout = gd._fullLayout, - sceneIds = Plots.getSubplotIds(fullLayout, GL3D), - size = fullLayout._size; + var fullLayout = gd._fullLayout; + var sceneIds = fullLayout._subplots[GL3D]; + var size = fullLayout._size; for(var i = 0; i < sceneIds.length; i++) { - var sceneLayout = fullLayout[sceneIds[i]], - domain = sceneLayout.domain, - scene = sceneLayout._scene; + var sceneLayout = fullLayout[sceneIds[i]]; + var domain = sceneLayout.domain; + var scene = sceneLayout._scene; var imageData = scene.toImage('png'); var image = fullLayout._glimages.append('svg:image'); @@ -130,7 +130,7 @@ exports.cleanId = function cleanId(id) { }; exports.updateFx = function(fullLayout) { - var subplotIds = Plots.getSubplotIds(fullLayout, GL3D); + var subplotIds = fullLayout._subplots[GL3D]; for(var i = 0; i < subplotIds.length; i++) { var subplotObj = fullLayout[subplotIds[i]]._scene; diff --git a/src/plots/layout_attributes.js b/src/plots/layout_attributes.js index 3e07d25be92..a62f555385f 100644 --- a/src/plots/layout_attributes.js +++ b/src/plots/layout_attributes.js @@ -134,7 +134,7 @@ module.exports = { description: 'Sets the color of paper where the graph is drawn.' }, plot_bgcolor: { - // defined here, but set in Axes.supplyLayoutDefaults + // defined here, but set in cartesian.supplyLayoutDefaults // because it needs to know if there are (2D) axes or not valType: 'color', role: 'style', diff --git a/src/plots/mapbox/index.js b/src/plots/mapbox/index.js index b417cb59729..7a8b012ff06 100644 --- a/src/plots/mapbox/index.js +++ b/src/plots/mapbox/index.js @@ -12,7 +12,7 @@ var mapboxgl = require('mapbox-gl'); var Lib = require('../../lib'); -var Plots = require('../plots'); +var getSubplotCalcData = require('../../plots/get_data').getSubplotCalcData; var xmlnsNamespaces = require('../../constants/xmlns_namespaces'); var createMapbox = require('./mapbox'); @@ -49,16 +49,16 @@ exports.layoutAttributes = require('./layout_attributes'); exports.supplyLayoutDefaults = require('./layout_defaults'); exports.plot = function plotMapbox(gd) { - var fullLayout = gd._fullLayout, - calcData = gd.calcdata, - mapboxIds = Plots.getSubplotIds(fullLayout, MAPBOX); + var fullLayout = gd._fullLayout; + var calcData = gd.calcdata; + var mapboxIds = fullLayout._subplots[MAPBOX]; var accessToken = findAccessToken(gd, mapboxIds); mapboxgl.accessToken = accessToken; for(var i = 0; i < mapboxIds.length; i++) { var id = mapboxIds[i], - subplotCalcData = Plots.getSubplotCalcData(calcData, MAPBOX, id), + subplotCalcData = getSubplotCalcData(calcData, MAPBOX, id), opts = fullLayout[id], mapbox = opts._subplot; @@ -91,7 +91,7 @@ exports.plot = function plotMapbox(gd) { }; exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { - var oldMapboxKeys = Plots.getSubplotIds(oldFullLayout, MAPBOX); + var oldMapboxKeys = oldFullLayout._subplots[MAPBOX] || []; for(var i = 0; i < oldMapboxKeys.length; i++) { var oldMapboxKey = oldMapboxKeys[i]; @@ -103,9 +103,9 @@ exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) }; exports.toSVG = function(gd) { - var fullLayout = gd._fullLayout, - subplotIds = Plots.getSubplotIds(fullLayout, MAPBOX), - size = fullLayout._size; + var fullLayout = gd._fullLayout; + var subplotIds = fullLayout._subplots[MAPBOX]; + var size = fullLayout._size; for(var i = 0; i < subplotIds.length; i++) { var opts = fullLayout[subplotIds[i]], @@ -157,7 +157,7 @@ function findAccessToken(gd, mapboxIds) { } exports.updateFx = function(fullLayout) { - var subplotIds = Plots.getSubplotIds(fullLayout, MAPBOX); + var subplotIds = fullLayout._subplots[MAPBOX]; for(var i = 0; i < subplotIds.length; i++) { var subplotObj = fullLayout[subplotIds[i]]._subplot; diff --git a/src/plots/plots.js b/src/plots/plots.js index 53cb80e65b4..e7a31c22aa2 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -15,6 +15,7 @@ var isNumeric = require('fast-isnumeric'); var Plotly = require('../plotly'); var PlotSchema = require('../plot_api/plot_schema'); var Registry = require('../registry'); +var axisIDs = require('../plots/cartesian/axis_ids'); var Lib = require('../lib'); var _ = Lib._; var Color = require('../components/color'); @@ -38,7 +39,6 @@ plots.layoutAttributes = require('./layout_attributes'); // TODO make this a plot attribute? plots.fontWeight = 'normal'; -var subplotsRegistry = plots.subplotsRegistry; var transformsRegistry = plots.transformsRegistry; var ErrorBars = require('../components/errorbars'); @@ -49,143 +49,6 @@ plots.computeAPICommandBindings = commandModule.computeAPICommandBindings; plots.manageCommandObserver = commandModule.manageCommandObserver; plots.hasSimpleAPICommandBindings = commandModule.hasSimpleAPICommandBindings; -/** - * Find subplot ids in data. - * Meant to be used in the defaults step. - * - * Use plots.getSubplotIds to grab the current - * subplot ids later on in Plotly.plot. - * - * @param {array} data plotly data array - * (intended to be _fullData, but does not have to be). - * @param {string} type subplot type to look for. - * - * @return {array} list of subplot ids (strings). - * N.B. these ids possibly un-ordered. - * - * TODO incorporate cartesian/gl2d axis finders in this paradigm. - */ -plots.findSubplotIds = function findSubplotIds(data, type) { - var subplotIds = []; - - if(!plots.subplotsRegistry[type]) return subplotIds; - - var attr = plots.subplotsRegistry[type].attr; - - for(var i = 0; i < data.length; i++) { - var trace = data[i]; - - if(plots.traceIs(trace, type) && subplotIds.indexOf(trace[attr]) === -1) { - subplotIds.push(trace[attr]); - } - } - - return subplotIds; -}; - -/** - * Get the ids of the current subplots. - * - * @param {object} layout plotly full layout object. - * @param {string} type subplot type to look for. - * - * @return {array} list of ordered subplot ids (strings). - * - */ -plots.getSubplotIds = function getSubplotIds(layout, type) { - var _module = plots.subplotsRegistry[type]; - - if(!_module) return []; - - // layout must be 'fullLayout' here - if(type === 'cartesian' && (!layout._has || !layout._has('cartesian'))) return []; - if(type === 'gl2d' && (!layout._has || !layout._has('gl2d'))) return []; - if(type === 'cartesian' || type === 'gl2d') { - return Object.keys(layout._plots || {}); - } - - var attrRegex = _module.attrRegex, - layoutKeys = Object.keys(layout), - subplotIds = []; - - for(var i = 0; i < layoutKeys.length; i++) { - var layoutKey = layoutKeys[i]; - - if(attrRegex.test(layoutKey)) subplotIds.push(layoutKey); - } - - // order the ids - var idLen = _module.idRoot.length; - subplotIds.sort(function(a, b) { - var aNum = +(a.substr(idLen) || 1), - bNum = +(b.substr(idLen) || 1); - return aNum - bNum; - }); - - return subplotIds; -}; - -/** - * Get the data trace(s) associated with a given subplot. - * - * @param {array} data plotly full data array. - * @param {string} type subplot type to look for. - * @param {string} subplotId subplot id to look for. - * - * @return {array} list of trace objects. - * - */ -plots.getSubplotData = function getSubplotData(data, type, subplotId) { - if(!plots.subplotsRegistry[type]) return []; - - var attr = plots.subplotsRegistry[type].attr, - subplotData = [], - trace; - - for(var i = 0; i < data.length; i++) { - trace = data[i]; - - if(type === 'gl2d' && plots.traceIs(trace, 'gl2d')) { - var spmatch = Plotly.Axes.subplotMatch, - subplotX = 'x' + subplotId.match(spmatch)[1], - subplotY = 'y' + subplotId.match(spmatch)[2]; - - if(trace[attr[0]] === subplotX && trace[attr[1]] === subplotY) { - subplotData.push(trace); - } - } - else { - if(trace[attr] === subplotId) subplotData.push(trace); - } - } - - return subplotData; -}; - -/** - * Get calcdata traces(s) associated with a given subplot - * - * @param {array} calcData (as in gd.calcdata) - * @param {string} type subplot type - * @param {string} subplotId subplot id to look for - * - * @return {array} array of calcdata traces - */ -plots.getSubplotCalcData = function(calcData, type, subplotId) { - if(!plots.subplotsRegistry[type]) return []; - - var attr = plots.subplotsRegistry[type].attr; - var subplotCalcData = []; - - for(var i = 0; i < calcData.length; i++) { - var calcTrace = calcData[i], - trace = calcTrace[0].trace; - - if(trace[attr] === subplotId) subplotCalcData.push(calcTrace); - } - - return subplotCalcData; -}; // in some cases the browser doesn't seem to know how big // the text is at first, so it needs to draw it, @@ -484,6 +347,11 @@ plots.supplyDefaults = function(gd) { // keep track of how many traces are inputted newFullLayout._dataLength = newData.length; + // clear the lists of trace and baseplot modules, and subplots + newFullLayout._modules = []; + newFullLayout._basePlotModules = []; + newFullLayout._subplots = emptySubplotLists(); + // then do the data newFullLayout._globalTransforms = (gd._context || {}).globalTransforms; plots.supplyDataDefaults(newData, newFullData, newLayout, newFullLayout); @@ -529,7 +397,7 @@ plots.supplyDefaults = function(gd) { plots.doAutoMargin(gd); // set scale after auto margin routine - var axList = Plotly.Axes.list(gd); + var axList = axisIDs.list(gd); for(i = 0; i < axList.length; i++) { var ax = axList[i]; ax.setScale(); @@ -551,6 +419,48 @@ plots.supplyDefaults = function(gd) { } }; +/** + * Make a container for collecting subplots we need to display. + * + * Finds all subplot types we need to enumerate once and caches it, + * but makes a new output object each time. + * Single-trace subplots (which have no `id`) such as pie, table, etc + * do not need to be collected because we just draw all visible traces. + */ +var collectableSubplotTypes; +function emptySubplotLists() { + var out = {}; + var i, j; + + if(!collectableSubplotTypes) { + collectableSubplotTypes = []; + + var subplotsRegistry = Registry.subplotsRegistry; + + for(var subplotType in subplotsRegistry) { + var subplotModule = subplotsRegistry[subplotType]; + var subplotAttr = subplotModule.attr; + + if(subplotAttr) { + collectableSubplotTypes.push(subplotType); + + // special case, currently just for cartesian: + // we need to enumerate axes, not just subplots + if(Array.isArray(subplotAttr)) { + for(j = 0; j < subplotAttr.length; j++) { + Lib.pushUnique(collectableSubplotTypes, subplotAttr[j]); + } + } + } + } + } + + for(i = 0; i < collectableSubplotTypes.length; i++) { + out[collectableSubplotTypes[i]] = []; + } + return out; +} + function remapTransformedArrays(cd0, newTrace) { var oldTrace = cd0.trace; var arrayAttrs = oldTrace._arrayAttrs; @@ -763,23 +673,29 @@ plots.cleanPlot = function(newFullData, newFullLayout, oldFullData, oldFullLayou }; plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { - var oldSubplots = oldFullLayout._plots || {}, - newSubplots = newFullLayout._plots = {}; + var oldSubplots = oldFullLayout._plots || {}; + var newSubplots = newFullLayout._plots = {}; + var newSubplotList = newFullLayout._subplots; var mockGd = { _fullData: newFullData, _fullLayout: newFullLayout }; - var ids = Plotly.Axes.getSubplots(mockGd); + var ids = newSubplotList.cartesian.concat(newSubplotList.gl2d || []); - var i; + var i, j, id, ax; + + // sort subplot lists + for(var subplotType in newSubplotList) { + newSubplotList[subplotType].sort(Lib.subplotSort); + } for(i = 0; i < ids.length; i++) { - var id = ids[i]; + id = ids[i]; var oldSubplot = oldSubplots[id]; - var xaxis = Plotly.Axes.getFromId(mockGd, id, 'x'); - var yaxis = Plotly.Axes.getFromId(mockGd, id, 'y'); + var xaxis = axisIDs.getFromId(mockGd, id, 'x'); + var yaxis = axisIDs.getFromId(mockGd, id, 'y'); var plotinfo; if(oldSubplot) { @@ -812,7 +728,7 @@ plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLa // find this out here, once of for all. plotinfo._hasClipOnAxisFalse = false; - for(var j = 0; j < newFullData.length; j++) { + for(j = 0; j < newFullData.length; j++) { var trace = newFullData[j]; if( @@ -828,13 +744,13 @@ plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLa // while we're at it, link overlaying axes to their main axes and // anchored axes to the axes they're anchored to - var axList = Plotly.Axes.list(mockGd, null, true); + var axList = axisIDs.list(mockGd, null, true); for(i = 0; i < axList.length; i++) { - var ax = axList[i]; + ax = axList[i]; var mainAx = null; if(ax.overlaying) { - mainAx = Plotly.Axes.getFromId(mockGd, ax.overlaying); + mainAx = axisIDs.getFromId(mockGd, ax.overlaying); // you cannot overlay an axis that's already overlaying another if(mainAx && mainAx.overlaying) { @@ -856,7 +772,44 @@ plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLa ax._anchorAxis = ax.anchor === 'free' ? null : - Plotly.Axes.getFromId(mockGd, ax.anchor); + axisIDs.getFromId(mockGd, ax.anchor); + } + + for(i = 0; i < axList.length; i++) { + // Figure out which subplot to draw ticks, labels, & axis lines on + // do this as a separate loop so we already have all the + // _mainAxis and _anchorAxis links set + ax = axList[i]; + var isX = ax._id.charAt(0) === 'x'; + var anchorAx = ax._mainAxis._anchorAxis; + var mainSubplotID = ''; + var nextBestMainSubplotID = ''; + var anchorID = ''; + // First try the main ID with the anchor + if(anchorAx) { + anchorID = anchorAx._mainAxis._id; + mainSubplotID = isX ? (ax._id + anchorID) : (anchorID + ax._id); + } + // Then look for a subplot with the counteraxis overlaying the anchor + // If that fails just use the first subplot including this axis + if(!mainSubplotID || ids.indexOf(mainSubplotID) === -1) { + mainSubplotID = ''; + for(j = 0; j < ids.length; j++) { + id = ids[j]; + var yIndex = id.indexOf('y'); + var idPart = isX ? id.substr(0, yIndex) : id.substr(yIndex); + var counterPart = isX ? id.substr(yIndex) : id.substr(0, yIndex); + if(idPart === ax._id) { + if(!nextBestMainSubplotID) nextBestMainSubplotID = id; + var counterAx = axisIDs.getFromId(mockGd, counterPart); + if(anchorID && counterAx.overlaying === anchorID) { + mainSubplotID = id; + break; + } + } + } + } + ax._mainSubplot = mainSubplotID || nextBestMainSubplotID; } }; @@ -906,10 +859,12 @@ plots.clearExpandedTraceDefaultColors = function(trace) { plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { + var modules = fullLayout._modules; + var basePlotModules = fullLayout._basePlotModules; + var cnt = 0; + var colorCnt = 0; + var i, fullTrace, trace; - var modules = fullLayout._modules = [], - basePlotModules = fullLayout._basePlotModules = [], - cnt = 0; fullLayout._transformModules = []; @@ -919,10 +874,19 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { var _module = fullTrace._module; if(!_module) return; - Lib.pushUnique(modules, _module); + if(fullTrace.visible === true) Lib.pushUnique(modules, _module); Lib.pushUnique(basePlotModules, fullTrace._module.basePlotModule); cnt++; + + // TODO: do we really want color not to increment for explicitly invisible traces? + // This logic is weird, but matches previous behavior: traces that you explicitly + // set to visible:false do not increment the color, but traces WE determine to be + // empty or invalid (and thus set to visible:false) DO increment color. + // I kind of think we should just let all traces increment color, visible or not. + // see mock: axes-autotype-empty vs. a test of restyling visible: false that + // I can't find right now... + if(fullTrace._input.visible !== false) colorCnt++; } var carpetIndex = {}; @@ -930,7 +894,7 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { for(i = 0; i < dataIn.length; i++) { trace = dataIn[i]; - fullTrace = plots.supplyTraceDefaults(trace, cnt, fullLayout, i); + fullTrace = plots.supplyTraceDefaults(trace, colorCnt, fullLayout, i); fullTrace.index = i; fullTrace._input = trace; @@ -1076,49 +1040,63 @@ plots.supplyFrameDefaults = function(frameIn) { return frameOut; }; -plots.supplyTraceDefaults = function(traceIn, traceOutIndex, layout, traceInIndex) { +plots.supplyTraceDefaults = function(traceIn, colorIndex, layout, traceInIndex) { var colorway = layout.colorway || Color.defaults; var traceOut = {}, - defaultColor = colorway[traceOutIndex % colorway.length]; + defaultColor = colorway[colorIndex % colorway.length]; + + var i; function coerce(attr, dflt) { return Lib.coerce(traceIn, traceOut, plots.attributes, attr, dflt); } - function coerceSubplotAttr(subplotType, subplotAttr) { - if(!plots.traceIs(traceOut, subplotType)) return; - - return Lib.coerce(traceIn, traceOut, - plots.subplotsRegistry[subplotType].attributes, subplotAttr); - } - var visible = coerce('visible'); coerce('type'); coerce('uid'); coerce('name', layout._traceWord + ' ' + traceInIndex); - // coerce subplot attributes of all registered subplot types - var subplotTypes = Object.keys(subplotsRegistry); - for(var i = 0; i < subplotTypes.length; i++) { - var subplotType = subplotTypes[i]; - - // done below (only when visible is true) - // TODO unified this pattern - if(['cartesian', 'gl2d'].indexOf(subplotType) !== -1) continue; - - var attr = subplotsRegistry[subplotType].attr; + // we want even invisible traces to make their would-be subplots visible + // so coerce the subplot id(s) now no matter what + var _module = plots.getModule(traceOut); + traceOut._module = _module; + if(_module) { + var basePlotModule = _module.basePlotModule; + var subplotAttr = basePlotModule.attr; + if(subplotAttr) { + var subplots = layout._subplots; + var subplotAttrs = basePlotModule.attributes; + var subplotId = ''; + + // TODO - currently if we draw an empty gl2d subplot, it draws + // nothing then gets stuck and you can't get it back without newPlot + // sort this out in the regl refactor? but for now just drop empty gl2d subplots + if(basePlotModule.name !== 'gl2d' || visible) { + if(Array.isArray(subplotAttr)) { + for(i = 0; i < subplotAttr.length; i++) { + var attri = subplotAttr[i]; + var vali = Lib.coerce(traceIn, traceOut, subplotAttrs, attri); + + if(subplots[attri]) Lib.pushUnique(subplots[attri], vali); + subplotId += vali; + } + } + else { + subplotId = Lib.coerce(traceIn, traceOut, subplotAttrs, subplotAttr); + } - if(attr) coerceSubplotAttr(subplotType, attr); + if(subplots[basePlotModule.name]) { + Lib.pushUnique(subplots[basePlotModule.name], subplotId); + } + } + } } if(visible) { coerce('customdata'); coerce('ids'); - var _module = plots.getModule(traceOut); - traceOut._module = _module; - if(plots.traceIs(traceOut, 'showLegend')) { coerce('showlegend'); coerce('legendgroup'); @@ -1138,12 +1116,6 @@ plots.supplyTraceDefaults = function(traceIn, traceOutIndex, layout, traceInInde if(!plots.traceIs(traceOut, 'noOpacity')) coerce('opacity'); - coerceSubplotAttr('cartesian', 'xaxis'); - coerceSubplotAttr('cartesian', 'yaxis'); - - coerceSubplotAttr('gl2d', 'xaxis'); - coerceSubplotAttr('gl2d', 'yaxis'); - if(plots.traceIs(traceOut, 'notLegendIsolatable')) { // This clears out the legendonly state for traces like carpet that // cannot be isolated in the legend @@ -1377,21 +1349,37 @@ function calculateReservedMargins(margins) { } plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData, transitionData) { - var i, _module; + var componentsRegistry = Registry.componentsRegistry; + var basePlotModules = layoutOut._basePlotModules; + var component, i, _module; + + var Cartesian = Registry.subplotsRegistry.cartesian; + + // check if any components need to add more base plot modules + // that weren't captured by traces + for(component in componentsRegistry) { + _module = componentsRegistry[component]; + + if(_module.includeBasePlot) { + _module.includeBasePlot(layoutIn, layoutOut); + } + } - // can't be be part of basePlotModules loop - // in order to handle the orphan axes case - Plotly.Axes.supplyLayoutDefaults(layoutIn, layoutOut, fullData); + // make sure we *at least* have some cartesian axes + if(!basePlotModules.length) { + basePlotModules.push(Cartesian); + } + + // ensure all cartesian axes have at least one subplot + if(layoutOut._has('cartesian')) { + Cartesian.finalizeSubplots(layoutIn, layoutOut); + } // base plot module layout defaults - var basePlotModules = layoutOut._basePlotModules; for(i = 0; i < basePlotModules.length; i++) { _module = basePlotModules[i]; - // done above already - if(_module.name === 'cartesian') continue; - - // e.g. gl2d does not have a layout-defaults step + // e.g. pie does not have a layout-defaults step if(_module.supplyLayoutDefaults) { _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData); } @@ -1417,9 +1405,8 @@ plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData, trans } } - var components = Object.keys(Registry.componentsRegistry); - for(i = 0; i < components.length; i++) { - _module = Registry.componentsRegistry[components[i]]; + for(component in componentsRegistry) { + _module = componentsRegistry[component]; if(_module.supplyLayoutDefaults) { _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData); @@ -1603,10 +1590,7 @@ plots.doAutoMargin = function(gd) { // now cycle through all the combinations of l and r // (and t and b) to find the required margins - var pmKeys = Object.keys(pm); - - for(var i = 0; i < pmKeys.length; i++) { - var k1 = pmKeys[i]; + for(var k1 in pm) { var pushleft = pm[k1].l || {}, pushbottom = pm[k1].b || {}, @@ -1615,9 +1599,7 @@ plots.doAutoMargin = function(gd) { fb = pushbottom.val, pb = pushbottom.size; - for(var j = 0; j < pmKeys.length; j++) { - var k2 = pmKeys[j]; - + for(var k2 in pm) { if(isNumeric(pl) && pm[k2].r) { var fr = pm[k2].r.val, pr = pm[k2].r.size; @@ -2283,7 +2265,7 @@ plots.transition = function(gd, data, layout, traces, frameOpts, transitionOpts) }; plots.doCalcdata = function(gd, traces) { - var axList = Plotly.Axes.list(gd), + var axList = axisIDs.list(gd), fullData = gd._fullData, fullLayout = gd._fullLayout; @@ -2466,31 +2448,24 @@ plots.generalUpdatePerTraceModule = function(subplot, subplotCalcData, subplotLa } } - var moduleNamesOld = Object.keys(traceHashOld); - var moduleNames = Object.keys(traceHash); - // when a trace gets deleted, make sure that its module's // plot method is called so that it is properly // removed from the DOM. - for(i = 0; i < moduleNamesOld.length; i++) { - var moduleName = moduleNamesOld[i]; + for(var moduleNameOld in traceHashOld) { - if(moduleNames.indexOf(moduleName) === -1) { - var fakeCalcTrace = traceHashOld[moduleName][0], + if(!traceHash[moduleNameOld]) { + var fakeCalcTrace = traceHashOld[moduleNameOld][0], fakeTrace = fakeCalcTrace[0].trace; fakeTrace.visible = false; - traceHash[moduleName] = [fakeCalcTrace]; + traceHash[moduleNameOld] = [fakeCalcTrace]; } } - // update list of module names to include 'fake' traces added above - moduleNames = Object.keys(traceHash); - // call module plot method - for(i = 0; i < moduleNames.length; i++) { - var moduleCalcData = traceHash[moduleNames[i]], - _module = moduleCalcData[0][0].trace._module; + for(var moduleName in traceHash) { + var moduleCalcData = traceHash[moduleName]; + var _module = moduleCalcData[0][0].trace._module; _module.plot(subplot, filterVisible(moduleCalcData), subplotLayout); } diff --git a/src/plots/subplot_defaults.js b/src/plots/subplot_defaults.js index 1da3202973d..3e1d49900d1 100644 --- a/src/plots/subplot_defaults.js +++ b/src/plots/subplot_defaults.js @@ -10,7 +10,6 @@ 'use strict'; var Lib = require('../lib'); -var Plots = require('./plots'); /** @@ -41,13 +40,13 @@ var Plots = require('./plots'); * } */ module.exports = function handleSubplotDefaults(layoutIn, layoutOut, fullData, opts) { - var subplotType = opts.type, - subplotAttributes = opts.attributes, - handleDefaults = opts.handleDefaults, - partition = opts.partition || 'x'; + var subplotType = opts.type; + var subplotAttributes = opts.attributes; + var handleDefaults = opts.handleDefaults; + var partition = opts.partition || 'x'; - var ids = Plots.findSubplotIds(fullData, subplotType), - idsLength = ids.length; + var ids = layoutOut._subplots[subplotType]; + var idsLength = ids.length; var subplotLayoutIn, subplotLayoutOut; diff --git a/src/plots/ternary/index.js b/src/plots/ternary/index.js index a4227ecbc01..d6a210d9d03 100644 --- a/src/plots/ternary/index.js +++ b/src/plots/ternary/index.js @@ -11,7 +11,7 @@ var Ternary = require('./ternary'); -var Plots = require('../../plots/plots'); +var getSubplotCalcData = require('../../plots/get_data').getSubplotCalcData; var counterRegex = require('../../lib').counterRegex; var TERNARY = 'ternary'; @@ -30,13 +30,13 @@ exports.layoutAttributes = require('./layout/layout_attributes'); exports.supplyLayoutDefaults = require('./layout/defaults'); exports.plot = function plotTernary(gd) { - var fullLayout = gd._fullLayout, - calcData = gd.calcdata, - ternaryIds = Plots.getSubplotIds(fullLayout, TERNARY); + var fullLayout = gd._fullLayout; + var calcData = gd.calcdata; + var ternaryIds = fullLayout._subplots[TERNARY]; for(var i = 0; i < ternaryIds.length; i++) { var ternaryId = ternaryIds[i], - ternaryCalcData = Plots.getSubplotCalcData(calcData, TERNARY, ternaryId), + ternaryCalcData = getSubplotCalcData(calcData, TERNARY, ternaryId), ternary = fullLayout[ternaryId]._subplot; // If ternary is not instantiated, create one! @@ -57,7 +57,7 @@ exports.plot = function plotTernary(gd) { }; exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { - var oldTernaryKeys = Plots.getSubplotIds(oldFullLayout, TERNARY); + var oldTernaryKeys = oldFullLayout._subplots[TERNARY] || []; for(var i = 0; i < oldTernaryKeys.length; i++) { var oldTernaryKey = oldTernaryKeys[i]; diff --git a/src/plots/ternary/ternary.js b/src/plots/ternary/ternary.js index 1240c653c60..9068a09576f 100644 --- a/src/plots/ternary/ternary.js +++ b/src/plots/ternary/ternary.js @@ -156,6 +156,9 @@ proto.updateLayers = function(ternaryLayout) { } else if(d === 'grids') { grids.forEach(function(d) { layers[d] = s.append('g').classed('grid ' + d, true); + + var fictID = (d === 'bgrid') ? 'x' : 'y'; + layers[d].append('g').classed(fictID, true); }); } }); @@ -318,16 +321,19 @@ proto.adjustLayout = function(ternaryLayout, graphSize) { // TODO: shift axes to accommodate linewidth*sin(30) tick mark angle - var bTransform = 'translate(' + x0 + ',' + (y0 + h) + ')'; + // TODO: there's probably an easier way to handle these translations/offsets now... + var bTransform = 'translate(' + (x0 - baxis._offset) + ',' + (y0 + h) + ')'; _this.layers.baxis.attr('transform', bTransform); _this.layers.bgrid.attr('transform', bTransform); - var aTransform = 'translate(' + (x0 + w / 2) + ',' + y0 + ')rotate(30)'; + var aTransform = 'translate(' + (x0 + w / 2) + ',' + y0 + + ')rotate(30)translate(0,-' + aaxis._offset + ')'; _this.layers.aaxis.attr('transform', aTransform); _this.layers.agrid.attr('transform', aTransform); - var cTransform = 'translate(' + (x0 + w / 2) + ',' + y0 + ')rotate(-30)'; + var cTransform = 'translate(' + (x0 + w / 2) + ',' + y0 + + ')rotate(-30)translate(0,-' + caxis._offset + ')'; _this.layers.caxis.attr('transform', cTransform); _this.layers.cgrid.attr('transform', cTransform); diff --git a/src/snapshot/cloneplot.js b/src/snapshot/cloneplot.js index a03e2667b6c..0b4aff611ab 100644 --- a/src/snapshot/cloneplot.js +++ b/src/snapshot/cloneplot.js @@ -10,7 +10,6 @@ 'use strict'; var Lib = require('../lib'); -var Plots = require('../plots/plots'); var extendFlat = Lib.extendFlat; var extendDeep = Lib.extendDeep; @@ -101,8 +100,11 @@ module.exports = function clonePlot(graphObj, options) { } } - var sceneIds = Plots.getSubplotIds(newLayout, 'gl3d'); - + // TODO: does this scene modification really belong here? + // If we still need it, can it move into the gl3d module? + var sceneIds = Object.keys(newLayout).filter(function(key) { + return key.match(/^scene\d*$/); + }); if(sceneIds.length) { var axesImageOverride = {}; if(options.tileClass === 'thumbnail') { diff --git a/src/traces/parcoords/base_plot.js b/src/traces/parcoords/base_plot.js index c562787b289..fd6ce675b5b 100644 --- a/src/traces/parcoords/base_plot.js +++ b/src/traces/parcoords/base_plot.js @@ -9,22 +9,22 @@ 'use strict'; var d3 = require('d3'); -var Plots = require('../../plots/plots'); +var getModuleCalcData = require('../../plots/get_data').getModuleCalcData; var parcoordsPlot = require('./plot'); var xmlnsNamespaces = require('../../constants/xmlns_namespaces'); -exports.name = 'parcoords'; +var PARCOORDS = 'parcoords'; -exports.attr = 'type'; +exports.name = PARCOORDS; exports.plot = function(gd) { - var calcData = Plots.getSubplotCalcData(gd.calcdata, 'parcoords', 'parcoords'); + var calcData = getModuleCalcData(gd.calcdata, PARCOORDS); if(calcData.length) parcoordsPlot(gd, calcData); }; exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { - var hadParcoords = (oldFullLayout._has && oldFullLayout._has('parcoords')); - var hasParcoords = (newFullLayout._has && newFullLayout._has('parcoords')); + var hadParcoords = (oldFullLayout._has && oldFullLayout._has(PARCOORDS)); + var hasParcoords = (newFullLayout._has && newFullLayout._has(PARCOORDS)); if(hadParcoords && !hasParcoords) { oldFullLayout._paperdiv.selectAll('.parcoords').remove(); diff --git a/src/traces/sankey/base_plot.js b/src/traces/sankey/base_plot.js index 942dfd58b6e..7126f50fa32 100644 --- a/src/traces/sankey/base_plot.js +++ b/src/traces/sankey/base_plot.js @@ -9,26 +9,26 @@ 'use strict'; var overrideAll = require('../../plot_api/edit_types').overrideAll; -var Plots = require('../../plots/plots'); +var getModuleCalcData = require('../../plots/get_data').getModuleCalcData; var plot = require('./plot'); var fxAttrs = require('../../components/fx/layout_attributes'); -exports.name = 'sankey'; +var SANKEY = 'sankey'; -exports.attr = 'type'; +exports.name = SANKEY; exports.baseLayoutAttrOverrides = overrideAll({ hoverlabel: fxAttrs.hoverlabel }, 'plot', 'nested'); exports.plot = function(gd) { - var calcData = Plots.getSubplotCalcData(gd.calcdata, 'sankey', 'sankey'); - if(calcData.length) plot(gd, calcData); + var calcData = getModuleCalcData(gd.calcdata, SANKEY); + plot(gd, calcData); }; exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { - var hadPlot = (oldFullLayout._has && oldFullLayout._has('sankey')); - var hasPlot = (newFullLayout._has && newFullLayout._has('sankey')); + var hadPlot = (oldFullLayout._has && oldFullLayout._has(SANKEY)); + var hasPlot = (newFullLayout._has && newFullLayout._has(SANKEY)); if(hadPlot && !hasPlot) { oldFullLayout._paperdiv.selectAll('.sankey').remove(); diff --git a/src/traces/table/base_plot.js b/src/traces/table/base_plot.js index c68e3de2dbd..89bd4f4db9d 100644 --- a/src/traces/table/base_plot.js +++ b/src/traces/table/base_plot.js @@ -8,21 +8,21 @@ 'use strict'; -var Plots = require('../../plots/plots'); +var getModuleCalcData = require('../../plots/get_data').getModuleCalcData; var tablePlot = require('./plot'); -exports.name = 'table'; +var TABLE = 'table'; -exports.attr = 'type'; +exports.name = TABLE; exports.plot = function(gd) { - var calcData = Plots.getSubplotCalcData(gd.calcdata, 'table', 'table'); + var calcData = getModuleCalcData(gd.calcdata, TABLE); if(calcData.length) tablePlot(gd, calcData); }; exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { - var hadTable = (oldFullLayout._has && oldFullLayout._has('table')); - var hasTable = (newFullLayout._has && newFullLayout._has('table')); + var hadTable = (oldFullLayout._has && oldFullLayout._has(TABLE)); + var hasTable = (newFullLayout._has && newFullLayout._has(TABLE)); if(hadTable && !hasTable) { oldFullLayout._paperdiv.selectAll('.table').remove(); diff --git a/tasks/test_syntax.js b/tasks/test_syntax.js index 9c6e9804bd6..2262d4ff095 100644 --- a/tasks/test_syntax.js +++ b/tasks/test_syntax.js @@ -261,7 +261,7 @@ function assertCircularDeps() { var logs = []; // see https://github.com/plotly/plotly.js/milestone/9 - var MAX_ALLOWED_CIRCULAR_DEPS = 17; + var MAX_ALLOWED_CIRCULAR_DEPS = 16; if(circularDeps.length > MAX_ALLOWED_CIRCULAR_DEPS) { console.log(circularDeps.join('\n')); diff --git a/test/image/baselines/20.png b/test/image/baselines/20.png index abfccd22f1b..2e8b83f7def 100644 Binary files a/test/image/baselines/20.png and b/test/image/baselines/20.png differ diff --git a/test/image/baselines/airfoil.png b/test/image/baselines/airfoil.png index 81c0340bdec..4edfcadbd74 100644 Binary files a/test/image/baselines/airfoil.png and b/test/image/baselines/airfoil.png differ diff --git a/test/image/baselines/cheater_contour.png b/test/image/baselines/cheater_contour.png index 2c3645d5778..8c7c33b9e57 100644 Binary files a/test/image/baselines/cheater_contour.png and b/test/image/baselines/cheater_contour.png differ diff --git a/test/image/baselines/empty.png b/test/image/baselines/empty.png index 6e9598c9fa5..d8494d3b276 100644 Binary files a/test/image/baselines/empty.png and b/test/image/baselines/empty.png differ diff --git a/test/image/baselines/geo_multiple-usa-choropleths.png b/test/image/baselines/geo_multiple-usa-choropleths.png index bfd37cca985..b90c2d32de0 100644 Binary files a/test/image/baselines/geo_multiple-usa-choropleths.png and b/test/image/baselines/geo_multiple-usa-choropleths.png differ diff --git a/test/image/baselines/gl3d_ibm-plot.png b/test/image/baselines/gl3d_ibm-plot.png index 946b58ea7d1..ba049929342 100644 Binary files a/test/image/baselines/gl3d_ibm-plot.png and b/test/image/baselines/gl3d_ibm-plot.png differ diff --git a/test/image/baselines/gl3d_opacity-surface.png b/test/image/baselines/gl3d_opacity-surface.png index ac6e2fd016c..b0ae82b5ad7 100644 Binary files a/test/image/baselines/gl3d_opacity-surface.png and b/test/image/baselines/gl3d_opacity-surface.png differ diff --git a/test/image/baselines/multiple_axes_double.png b/test/image/baselines/multiple_axes_double.png index e053363b4a8..a003e3754e7 100644 Binary files a/test/image/baselines/multiple_axes_double.png and b/test/image/baselines/multiple_axes_double.png differ diff --git a/test/image/baselines/multiple_axes_multiple.png b/test/image/baselines/multiple_axes_multiple.png index 7cf933fe0e7..d4bd58f54a1 100644 Binary files a/test/image/baselines/multiple_axes_multiple.png and b/test/image/baselines/multiple_axes_multiple.png differ diff --git a/test/image/baselines/overlaying-axis-lines.png b/test/image/baselines/overlaying-axis-lines.png index 7df0a3d4360..8d5a6d95ca5 100644 Binary files a/test/image/baselines/overlaying-axis-lines.png and b/test/image/baselines/overlaying-axis-lines.png differ diff --git a/test/image/baselines/ternary_axis_layers.png b/test/image/baselines/ternary_axis_layers.png index e2e02dbf1b1..36daec4b21d 100644 Binary files a/test/image/baselines/ternary_axis_layers.png and b/test/image/baselines/ternary_axis_layers.png differ diff --git a/test/image/baselines/titles-avoid-labels.png b/test/image/baselines/titles-avoid-labels.png index 58dbb96cef3..35166b37985 100644 Binary files a/test/image/baselines/titles-avoid-labels.png and b/test/image/baselines/titles-avoid-labels.png differ diff --git a/test/image/baselines/world-cals.png b/test/image/baselines/world-cals.png index 7bf5e99b49d..200ed69241a 100644 Binary files a/test/image/baselines/world-cals.png and b/test/image/baselines/world-cals.png differ diff --git a/test/jasmine/tests/annotations_test.js b/test/jasmine/tests/annotations_test.js index e37a965a6e2..babbe0da795 100644 --- a/test/jasmine/tests/annotations_test.js +++ b/test/jasmine/tests/annotations_test.js @@ -25,6 +25,11 @@ describe('Test annotations', function() { function _supply(layoutIn, layoutOut) { layoutOut = layoutOut || {}; layoutOut._has = Plots._hasPlotType.bind(layoutOut); + layoutOut._subplots = {xaxis: ['x', 'x2'], yaxis: ['y', 'y2']}; + ['xaxis', 'yaxis', 'xaxis2', 'yaxis2'].forEach(function(axName) { + if(!layoutOut[axName]) layoutOut[axName] = {type: 'linear', range: [0, 1]}; + Axes.setConvert(layoutOut[axName]); + }); Annotations.supplyLayoutDefaults(layoutIn, layoutOut); @@ -89,7 +94,6 @@ describe('Test annotations', function() { var layoutOut = { xaxis: { type: 'date', range: ['2000-01-01', '2016-01-01'] } }; - Axes.setConvert(layoutOut.xaxis); _supply(layoutIn, layoutOut); @@ -132,10 +136,6 @@ describe('Test annotations', function() { yaxis2: {type: 'category', range: [0, 1]} }; - ['xaxis', 'xaxis2', 'yaxis', 'yaxis2'].forEach(function(k) { - Axes.setConvert(layoutOut[k]); - }); - _supply(layoutIn, layoutOut); expect(layoutOut.annotations[0]._xclick).toBe(10, 'paper x'); @@ -721,8 +721,8 @@ describe('annotations autorange', function() { text: 'LT', x: -1, y: 3, - xref: 'x5', // will be converted to 'x' and xaxis should autorange - yref: 'y5', // same 'y' -> yaxis + xref: 'xq', // will be converted to 'x' and xaxis should autorange + yref: 'yz', // same 'y' -> yaxis ax: 50, ay: 50 }}); diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 2e33ddab044..9c05bf23584 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -8,8 +8,10 @@ var Color = require('@src/components/color'); var tinycolor = require('tinycolor2'); var handleTickValueDefaults = require('@src/plots/cartesian/tick_value_defaults'); +var Cartesian = require('@src/plots/cartesian'); var Axes = require('@src/plots/cartesian/axes'); var Fx = require('@src/components/fx'); +var supplyLayoutDefaults = require('@src/plots/cartesian/layout_defaults'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); @@ -180,13 +182,12 @@ describe('Test axes', function() { layoutOut = { _has: Plots._hasPlotType, _basePlotModules: [], - _dfltTitle: {x: 'x', y: 'y'} + _dfltTitle: {x: 'x', y: 'y'}, + _subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']} }; fullData = []; }); - var supplyLayoutDefaults = Axes.supplyLayoutDefaults; - it('should set undefined linewidth/linecolor if linewidth, linecolor or showline is not supplied', function() { layoutIn = { xaxis: {}, @@ -269,69 +270,6 @@ describe('Test axes', function() { expect(layoutOut.xaxis.zerolinecolor).toBe(undefined); }); - it('should detect orphan axes (lone axes case)', function() { - layoutIn = { - xaxis: {}, - yaxis: {} - }; - fullData = []; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut._basePlotModules[0].name).toEqual('cartesian'); - }); - - it('should detect orphan axes (gl2d trace conflict case)', function() { - layoutIn = { - xaxis: {}, - yaxis: {} - }; - fullData = [{ - type: 'scattergl', - xaxis: 'x', - yaxis: 'y' - }]; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut._basePlotModules).toEqual([]); - }); - - it('should detect orphan axes (gl2d + cartesian case)', function() { - layoutIn = { - xaxis2: {}, - yaxis2: {} - }; - fullData = [{ - type: 'scattergl', - xaxis: 'x', - yaxis: 'y' - }]; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut._basePlotModules[0].name).toEqual('cartesian'); - }); - - it('should detect orphan axes (gl3d present case)', function() { - layoutIn = { - xaxis: {}, - yaxis: {} - }; - layoutOut._basePlotModules = [ { name: 'gl3d' }]; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut._basePlotModules).toEqual([ { name: 'gl3d' }]); - }); - - it('should detect orphan axes (geo present case)', function() { - layoutIn = { - xaxis: {}, - yaxis: {} - }; - layoutOut._basePlotModules = [ { name: 'geo' }]; - - supplyLayoutDefaults(layoutIn, layoutOut, fullData); - expect(layoutOut._basePlotModules).toEqual([ { name: 'geo' }]); - }); - it('should use \'axis.color\' as default for \'axis.titlefont.color\'', function() { layoutIn = { xaxis: { color: 'red' }, @@ -339,7 +277,9 @@ describe('Test axes', function() { yaxis2: { titlefont: { color: 'yellow' } } }; - layoutOut.font = { color: 'blue' }, + layoutOut.font = { color: 'blue' }; + layoutOut._subplots.cartesian.push('xy2'); + layoutOut._subplots.yaxis.push('y2'); supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.xaxis.titlefont.color).toEqual('red'); @@ -353,6 +293,8 @@ describe('Test axes', function() { yaxis: { linecolor: 'blue' }, yaxis2: { showline: true } }; + layoutOut._subplots.cartesian.push('xy2'); + layoutOut._subplots.yaxis.push('y2'); supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.xaxis.linecolor).toEqual('red'); @@ -366,6 +308,8 @@ describe('Test axes', function() { yaxis: { zerolinecolor: 'blue' }, yaxis2: { showzeroline: true } }; + layoutOut._subplots.cartesian.push('xy2'); + layoutOut._subplots.yaxis.push('y2'); supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.xaxis.zerolinecolor).toEqual('red'); @@ -381,6 +325,8 @@ describe('Test axes', function() { yaxis: { gridcolor: 'blue' }, yaxis2: { showgrid: true } }; + layoutOut._subplots.cartesian.push('xy2'); + layoutOut._subplots.yaxis.push('y2'); var bgColor = Color.combine('yellow', 'green'), frac = 100 * (0xe - 0x4) / (0xf - 0x4); @@ -429,6 +375,8 @@ describe('Test axes', function() { yaxis2: { range: [1, 'b'] }, yaxis3: { range: [null, {}] } }; + layoutOut._subplots.cartesian.push('x2y2', 'xy3'); + layoutOut._subplots.yaxis.push('x2', 'y2', 'y3'); supplyLayoutDefaults(layoutIn, layoutOut, fullData); @@ -444,6 +392,8 @@ describe('Test axes', function() { yaxis: { range: ['1', 2] }, yaxis2: { range: [1, '2'] } }; + layoutOut._subplots.cartesian.push('x2y2'); + layoutOut._subplots.yaxis.push('x2', 'y2'); supplyLayoutDefaults(layoutIn, layoutOut, fullData); @@ -465,6 +415,8 @@ describe('Test axes', function() { xaxis4: {scaleanchor: 'y3', scaleratio: 7}, xaxis5: {scaleanchor: 'y3', scaleratio: 9} }; + layoutOut._subplots.cartesian.push('x2y2', 'x3y3', 'x4y3', 'x5y3'); + layoutOut._subplots.yaxis.push('x2', 'x3', 'x4', 'x5', 'y2', 'y3'); supplyLayoutDefaults(layoutIn, layoutOut, fullData); @@ -496,6 +448,8 @@ describe('Test axes', function() { xaxis4: {scaleanchor: 'x', scaleratio: 13}, // x<->x is OK now yaxis4: {scaleanchor: 'y', scaleratio: 17}, // y<->y is OK now }; + layoutOut._subplots.cartesian.push('x2y2', 'x3y3', 'x4y4'); + layoutOut._subplots.yaxis.push('x2', 'x3', 'x4', 'y2', 'y3', 'y4'); supplyLayoutDefaults(layoutIn, layoutOut, fullData); @@ -521,6 +475,8 @@ describe('Test axes', function() { yaxis: {scaleanchor: 'x4', scaleratio: 3}, // doesn't exist xaxis2: {scaleanchor: 'yaxis', scaleratio: 5} // must be an id, not a name }; + layoutOut._subplots.cartesian.push('x2y'); + layoutOut._subplots.yaxis.push('x2'); supplyLayoutDefaults(layoutIn, layoutOut, fullData); @@ -540,6 +496,8 @@ describe('Test axes', function() { xaxis2: {type: 'date', scaleanchor: 'y', scaleratio: 3}, yaxis2: {type: 'category', scaleanchor: 'x2', scaleratio: 5} }; + layoutOut._subplots.cartesian.push('x2y2'); + layoutOut._subplots.yaxis.push('x2', 'y2'); supplyLayoutDefaults(layoutIn, layoutOut, fullData); @@ -562,6 +520,8 @@ describe('Test axes', function() { xaxis2: {}, yaxis2: {scaleanchor: 'x', scaleratio: 5} }; + layoutOut._subplots.cartesian.push('x2y2'); + layoutOut._subplots.yaxis.push('x2', 'y2'); supplyLayoutDefaults(layoutIn, layoutOut, fullData); @@ -1089,7 +1049,7 @@ describe('Test axes', function() { describe('handleTickValueDefaults', function() { function mockSupplyDefaults(axIn, axOut, axType) { function coerce(attr, dflt) { - return Lib.coerce(axIn, axOut, Axes.layoutAttributes, attr, dflt); + return Lib.coerce(axIn, axOut, Cartesian.layoutAttributes, attr, dflt); } handleTickValueDefaults(axIn, axOut, coerce, axType); @@ -1282,7 +1242,8 @@ describe('Test axes', function() { xaxis: { range: [0, 0.5] }, yaxis: { range: [0, 0.5] }, xaxis2: { range: [0.5, 1] }, - yaxis2: { range: [0.5, 1] } + yaxis2: { range: [0.5, 1] }, + _subplots: {xaxis: ['x', 'x2'], yaxis: ['y', 'y2'], cartesian: ['xy', 'x2y2']} } }; }); @@ -1344,6 +1305,7 @@ describe('Test axes', function() { it('returns array of axes in fullLayout', function() { gd = { _fullLayout: { + _subplots: {xaxis: ['x'], yaxis: ['y', 'y2']}, xaxis: { _id: 'x' }, yaxis: { _id: 'y' }, yaxis2: { _id: 'y2' } @@ -1357,6 +1319,7 @@ describe('Test axes', function() { it('returns array of axes, including the ones in scenes', function() { gd = { _fullLayout: { + _subplots: {xaxis: [], yaxis: [], gl3d: ['scene', 'scene2']}, scene: { xaxis: { _id: 'x' }, yaxis: { _id: 'y' }, @@ -1380,6 +1343,7 @@ describe('Test axes', function() { it('returns array of axes, excluding the ones in scenes with only2d option', function() { gd = { _fullLayout: { + _subplots: {xaxis: ['x2'], yaxis: ['y2'], gl3d: ['scene']}, scene: { xaxis: { _id: 'x' }, yaxis: { _id: 'y' }, @@ -1397,11 +1361,11 @@ describe('Test axes', function() { it('returns array of axes, of particular ax letter with axLetter option', function() { gd = { _fullLayout: { + _subplots: {xaxis: ['x2'], yaxis: ['y2'], gl3d: ['scene']}, scene: { - xaxis: { _id: 'x' }, + xaxis: { _id: 'x', _thisIs3d: true }, yaxis: { _id: 'y' }, - zaxis: { _id: 'z' - } + zaxis: { _id: 'z' } }, xaxis2: { _id: 'x2' }, yaxis2: { _id: 'y2' } @@ -1409,54 +1373,28 @@ describe('Test axes', function() { }; expect(listFunc(gd, 'x')) - .toEqual([{ _id: 'x2' }, { _id: 'x' }]); + .toEqual([{ _id: 'x2' }, { _id: 'x', _thisIs3d: true }]); }); }); describe('getSubplots', function() { var getSubplots = Axes.getSubplots; - var gd; - - it('returns list of subplots ids (from data only)', function() { - gd = { - data: [ - { type: 'scatter' }, - { type: 'scattergl', xaxis: 'x2', yaxis: 'y2' } - ] - }; - - expect(getSubplots(gd)) - .toEqual(['xy', 'x2y2']); - }); - - it('returns list of subplots ids (from fullLayout only)', function() { - gd = { - _fullLayout: { - xaxis: { _id: 'x', anchor: 'y' }, - yaxis: { _id: 'y', anchor: 'x' }, - xaxis2: { _id: 'x2', anchor: 'y2' }, - yaxis2: { _id: 'y2', anchor: 'x2' } + var gd = { + _fullLayout: { + _subplots: { + cartesian: ['x2y2'], + gl2d: ['xy'] } - }; + } + }; + it('returns only what was prepopulated in fullLayout._subplots', function() { expect(getSubplots(gd)) .toEqual(['xy', 'x2y2']); }); it('returns list of subplots ids of particular axis with ax option', function() { - gd = { - data: [ - { type: 'scatter' }, - { type: 'scattergl', xaxis: 'x3', yaxis: 'y3' } - ], - _fullLayout: { - xaxis2: { _id: 'x2', anchor: 'y2' }, - yaxis2: { _id: 'y2', anchor: 'x2' }, - yaxis3: { _id: 'y3', anchor: 'free' } - } - }; - expect(getSubplots(gd, { _id: 'x' })) .toEqual(['xy']); }); diff --git a/test/jasmine/tests/cartesian_interact_test.js b/test/jasmine/tests/cartesian_interact_test.js index 3ef3fbd7fd1..4ba2c5e27dc 100644 --- a/test/jasmine/tests/cartesian_interact_test.js +++ b/test/jasmine/tests/cartesian_interact_test.js @@ -240,8 +240,8 @@ describe('axis zoom/pan and main plot zoom', function() { return Plotly.newPlot(gd, data, layout, config) .then(checkRanges({}, 'initial')) .then(function() { - expect(Object.keys(gd._fullLayout._plots)) - .toEqual(['xy', 'xy2', 'x2y', 'x3y3']); + expect(Object.keys(gd._fullLayout._plots).sort()) + .toEqual(['xy', 'xy2', 'x2y', 'x3y3'].sort()); // nsew, n, ns, s, w, ew, e, ne, nw, se, sw expect(document.querySelectorAll('.drag[data-subplot="xy"]').length).toBe(11); diff --git a/test/jasmine/tests/cartesian_test.js b/test/jasmine/tests/cartesian_test.js index 415ccc0e20d..6ebf32f3ef5 100644 --- a/test/jasmine/tests/cartesian_test.js +++ b/test/jasmine/tests/cartesian_test.js @@ -281,13 +281,15 @@ describe('subplot creation / deletion:', function() { it('should clear orphan subplot when adding traces to blank graph', function(done) { - function assertCartesianSubplot(len) { + function assertOrphanSubplot(len) { expect(d3.select('.subplot.xy').size()).toEqual(len); - expect(d3.select('.subplot.x2y2').size()).toEqual(len); - expect(d3.select('.x2title').size()).toEqual(len); - expect(d3.select('.x2title').size()).toEqual(len); expect(d3.select('.ytitle').size()).toEqual(len); expect(d3.select('.ytitle').size()).toEqual(len); + + // we only make one orphan subplot now + expect(d3.select('.subplot.x2y2').size()).toEqual(0); + expect(d3.select('.x2title').size()).toEqual(0); + expect(d3.select('.x2title').size()).toEqual(0); } Plotly.plot(gd, [], { @@ -297,7 +299,7 @@ describe('subplot creation / deletion:', function() { yaxis2: { title: 'Y2', anchor: 'x2' } }) .then(function() { - assertCartesianSubplot(1); + assertOrphanSubplot(1); return Plotly.addTraces(gd, [{ type: 'scattergeo', @@ -306,7 +308,7 @@ describe('subplot creation / deletion:', function() { }]); }) .then(function() { - assertCartesianSubplot(0); + assertOrphanSubplot(0); }) .catch(failTest) .then(done); @@ -485,11 +487,11 @@ describe('subplot creation / deletion:', function() { var info = d3.select('.infolayer'); expect(g.selectAll('.xtick').size()).toBe(xaxis[0], 'x tick cnt'); - expect(g.selectAll('.gridlayer > .xgrid').size()).toBe(xaxis[1], 'x gridline cnt'); + expect(g.selectAll('.gridlayer .xgrid').size()).toBe(xaxis[1], 'x gridline cnt'); expect(info.selectAll('.g-xtitle').size()).toBe(xaxis[2], 'x title cnt'); expect(g.selectAll('.ytick').size()).toBe(yaxis[0], 'y tick cnt'); - expect(g.selectAll('.gridlayer > .ygrid').size()).toBe(yaxis[1], 'y gridline cnt'); + expect(g.selectAll('.gridlayer .ygrid').size()).toBe(yaxis[1], 'y gridline cnt'); expect(info.selectAll('.g-ytitle').size()).toBe(yaxis[2], 'y title cnt'); } diff --git a/test/jasmine/tests/fx_test.js b/test/jasmine/tests/fx_test.js index 1c425b84357..1d7f7a05ecd 100644 --- a/test/jasmine/tests/fx_test.js +++ b/test/jasmine/tests/fx_test.js @@ -24,7 +24,11 @@ describe('Fx defaults', function() { it('should default (blank version)', function() { var layoutOut = _supply().layout; - expect(layoutOut.hovermode).toBe('closest', 'hovermode to closest'); + // we get a blank cartesian subplot that has no traces... + // so all traces are horizontal -> hovermode defaults to y + // we could add a special case to push this back to x, but + // it seems like it has no practical consequence. + expect(layoutOut.hovermode).toBe('y', 'hovermode to y'); expect(layoutOut.dragmode).toBe('zoom', 'dragmode to zoom'); }); diff --git a/test/jasmine/tests/geo_test.js b/test/jasmine/tests/geo_test.js index 51f8b1e901a..db0b9c71253 100644 --- a/test/jasmine/tests/geo_test.js +++ b/test/jasmine/tests/geo_test.js @@ -36,7 +36,7 @@ describe('Test Geo layout defaults', function() { var layoutIn, layoutOut, fullData; beforeEach(function() { - layoutOut = {}; + layoutOut = {_subplots: {geo: ['geo']}}; // needs a geo-ref in a trace in order to be detected fullData = [{ type: 'scattergeo', geo: 'geo' }]; @@ -242,6 +242,7 @@ describe('Test Geo layout defaults', function() { }); it('should add geo data-only geos into layoutIn (converse)', function() { + layoutOut._subplots.geo = []; layoutIn = {}; fullData = [{ type: 'scatter' }]; diff --git a/test/jasmine/tests/gl3dlayout_test.js b/test/jasmine/tests/gl3dlayout_test.js index cbcdaf72ea4..2540a53a6f5 100644 --- a/test/jasmine/tests/gl3dlayout_test.js +++ b/test/jasmine/tests/gl3dlayout_test.js @@ -18,7 +18,11 @@ describe('Test Gl3d layout defaults', function() { var supplyLayoutDefaults = Gl3d.supplyLayoutDefaults; beforeEach(function() { - layoutOut = { _basePlotModules: ['gl3d'], _dfltTitle: {x: 'xxx', y: 'yyy', colorbar: 'cbbb'} }; + layoutOut = { + _basePlotModules: ['gl3d'], + _dfltTitle: {x: 'xxx', y: 'yyy', colorbar: 'cbbb'}, + _subplots: {gl3d: ['scene']} + }; // needs a scene-ref in a trace in order to be detected fullData = [ { type: 'scatter3d', scene: 'scene' }]; @@ -239,6 +243,7 @@ describe('Test Gl3d layout defaults', function() { it('should add scene data-only scenes into layoutIn (converse)', function() { layoutIn = {}; + layoutOut._subplots.gl3d = []; fullData = [{ type: 'scatter' }]; supplyLayoutDefaults(layoutIn, layoutOut, fullData); diff --git a/test/jasmine/tests/gl_plot_interact_test.js b/test/jasmine/tests/gl_plot_interact_test.js index ff3120faa3d..69b0f93a62b 100644 --- a/test/jasmine/tests/gl_plot_interact_test.js +++ b/test/jasmine/tests/gl_plot_interact_test.js @@ -309,8 +309,8 @@ describe('Test gl3d plots', function() { .then(function() { expect(countCanvases()).toEqual(1); expect(gd.layout.scene).toEqual(sceneLayout); - expect(gd.layout.xaxis).toBeUndefined(); - expect(gd.layout.yaxis).toBeUndefined(); + expect(gd.layout.xaxis === undefined).toBe(true); + expect(gd.layout.yaxis === undefined).toBe(true); expect(gd._fullLayout._has('gl3d')).toBe(true); expect(gd._fullLayout.scene._scene).toBeDefined(); @@ -322,7 +322,7 @@ describe('Test gl3d plots', function() { expect(gd.layout.xaxis).toBeDefined(); expect(gd.layout.yaxis).toBeDefined(); expect(gd._fullLayout._has('gl3d')).toBe(false); - expect(gd._fullLayout.scene).toBeUndefined(); + expect(gd._fullLayout.scene === undefined).toBe(true); return Plotly.restyle(gd, 'type', 'scatter3d'); }) @@ -349,7 +349,7 @@ describe('Test gl3d plots', function() { .then(function() { expect(countCanvases()).toEqual(0); expect(gd._fullLayout._has('gl3d')).toBe(false); - expect(gd._fullLayout.scene).toBeUndefined(); + expect(gd._fullLayout.scene === undefined).toBe(true); }) .then(done); }); @@ -424,7 +424,7 @@ describe('Test gl3d modebar handlers', function() { var gd, modeBar; function assertScenes(cont, attr, val) { - var sceneIds = Plots.getSubplotIds(cont, 'gl3d'); + var sceneIds = cont._subplots.gl3d; sceneIds.forEach(function(sceneId) { var thisVal = Lib.nestedProperty(cont[sceneId], attr).get(); @@ -479,7 +479,7 @@ describe('Test gl3d modebar handlers', function() { expect(buttonZoom3d.isActive()).toBe(false); buttonZoom3d.click(); - assertScenes(gd.layout, 'dragmode', 'zoom'); + assertScenes(gd._fullLayout, 'dragmode', 'zoom'); expect(gd.layout.dragmode).toBe(undefined); expect(gd._fullLayout.dragmode).toBe('zoom'); expect(buttonTurntable.isActive()).toBe(false); @@ -500,7 +500,7 @@ describe('Test gl3d modebar handlers', function() { expect(buttonPan3d.isActive()).toBe(false); buttonPan3d.click(); - assertScenes(gd.layout, 'dragmode', 'pan'); + assertScenes(gd._fullLayout, 'dragmode', 'pan'); expect(gd.layout.dragmode).toBe(undefined); expect(gd._fullLayout.dragmode).toBe('zoom'); expect(buttonTurntable.isActive()).toBe(false); @@ -521,7 +521,7 @@ describe('Test gl3d modebar handlers', function() { expect(buttonOrbit.isActive()).toBe(false); buttonOrbit.click(); - assertScenes(gd.layout, 'dragmode', 'orbit'); + assertScenes(gd._fullLayout, 'dragmode', 'orbit'); expect(gd.layout.dragmode).toBe(undefined); expect(gd._fullLayout.dragmode).toBe('zoom'); expect(buttonTurntable.isActive()).toBe(false); @@ -946,7 +946,12 @@ describe('Test gl2d plots', function() { var OBJECT_PER_TRACE = 2; var objects = function() { - return gd._fullLayout._plots.xy._scene2d.glplot.objects; + try { + return gd._fullLayout._plots.xy._scene2d.glplot.objects; + } + catch(e) { + return []; + } }; Plotly.plot(gd, _mock) @@ -969,7 +974,7 @@ describe('Test gl2d plots', function() { return Plotly.restyle(gd, 'visible', false); }) .then(function() { - expect(gd._fullLayout._plots.xy._scene2d).toBeUndefined(); + expect(objects().length).toBe(0); return Plotly.restyle(gd, 'visible', true); }) @@ -977,6 +982,7 @@ describe('Test gl2d plots', function() { expect(objects().length).toEqual(OBJECT_PER_TRACE); expect(objects()[0].data.length).not.toEqual(0); }) + .catch(fail) .then(done); }); @@ -1603,13 +1609,13 @@ describe('Test gl3d annotations', function() { }) .then(function() { assertAnnotationCntPerScene('scene', 1); - assertAnnotationCntPerScene('scene2', 0); + assertAnnotationCntPerScene('scene2', 2); return Plotly.deleteTraces(gd, [0]); }) .then(function() { - assertAnnotationCntPerScene('scene', 0); - assertAnnotationCntPerScene('scene2', 0); + assertAnnotationCntPerScene('scene', 1); + assertAnnotationCntPerScene('scene2', 2); }) .catch(fail) .then(done); diff --git a/test/jasmine/tests/heatmap_test.js b/test/jasmine/tests/heatmap_test.js index a102fd9b857..1f0afd929c0 100644 --- a/test/jasmine/tests/heatmap_test.js +++ b/test/jasmine/tests/heatmap_test.js @@ -22,7 +22,8 @@ describe('heatmap supplyDefaults', function() { var defaultColor = '#444', layout = { font: Plots.layoutAttributes.font, - _dfltTitle: {colorbar: 'cb'} + _dfltTitle: {colorbar: 'cb'}, + _subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']} }; var supplyDefaults = Heatmap.supplyDefaults; diff --git a/test/jasmine/tests/layout_images_test.js b/test/jasmine/tests/layout_images_test.js index fb0b2743732..fca0b88747f 100644 --- a/test/jasmine/tests/layout_images_test.js +++ b/test/jasmine/tests/layout_images_test.js @@ -21,7 +21,10 @@ describe('Layout images', function() { beforeEach(function() { layoutIn = { images: [] }; - layoutOut = { _has: Plots._hasPlotType }; + layoutOut = { + _has: Plots._hasPlotType, + _subplots: {xaxis: [], yaxis: []} + }; }); it('should reject when there is no `source`', function() { diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index 12b61aaa31a..c9700131069 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -1968,6 +1968,22 @@ describe('Test lib.js:', function() { expect(function() { return Lib.relativeAttr('x.y.', 'z'); }).toThrow(); }); }); + + describe('subplotSort', function() { + it('puts xy subplots in the right order', function() { + var a = ['x10y', 'x10y20', 'x10y12', 'x10y2', 'xy', 'x2y12', 'xy2', 'xy15']; + a.sort(Lib.subplotSort); + expect(a).toEqual(['xy', 'xy2', 'xy15', 'x2y12', 'x10y', 'x10y2', 'x10y12', 'x10y20']); + }); + + it('puts simple subplots in the right order', function() { + ['scene', 'geo', 'ternary', 'mapbox'].forEach(function(v) { + var a = [v + '100', v + '43', v, v + '10', v + '2']; + a.sort(Lib.subplotSort); + expect(a).toEqual([v, v + '2', v + '10', v + '43', v + '100']); + }); + }); + }); }); describe('Queue', function() { diff --git a/test/jasmine/tests/mapbox_test.js b/test/jasmine/tests/mapbox_test.js index 67dff7bae4e..3ca613b6b36 100644 --- a/test/jasmine/tests/mapbox_test.js +++ b/test/jasmine/tests/mapbox_test.js @@ -32,7 +32,7 @@ describe('mapbox defaults', function() { var layoutIn, layoutOut, fullData; beforeEach(function() { - layoutOut = { font: { color: 'red' } }; + layoutOut = { font: { color: 'red' }, _subplots: {mapbox: ['mapbox']} }; // needs a mapbox-ref in a trace in order to be detected fullData = [{ type: 'scattermapbox', subplot: 'mapbox' }]; @@ -66,6 +66,7 @@ describe('mapbox defaults', function() { }; fullData.push({ type: 'scattermapbox', subplot: 'mapbox2' }); + layoutOut._subplots.mapbox.push('mapbox2'); supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.mapbox.style).toEqual('light'); @@ -317,7 +318,9 @@ describe('@noCI, mapbox plots', function() { expect(countVisibleTraces(gd, modes)).toEqual(2); Plotly.restyle(gd, 'visible', false).then(function() { - expect(gd._fullLayout.mapbox).toBeUndefined(); + expect(gd._fullLayout.mapbox === undefined).toBe(false); + + expect(countVisibleTraces(gd, modes)).toEqual(0); return Plotly.restyle(gd, 'visible', true); }) @@ -381,7 +384,7 @@ describe('@noCI, mapbox plots', function() { return Plotly.deleteTraces(gd, [0, 1, 2]); }) .then(function() { - expect(gd._fullLayout.mapbox).toBeUndefined(); + expect(gd._fullLayout.mapbox === undefined).toBe(true); done(); }); diff --git a/test/jasmine/tests/modebar_test.js b/test/jasmine/tests/modebar_test.js index 9f0bd48b54e..f17dbbd9f8e 100644 --- a/test/jasmine/tests/modebar_test.js +++ b/test/jasmine/tests/modebar_test.js @@ -25,12 +25,13 @@ describe('ModeBar', function() { return parent; } - function getMockGraphInfo() { + function getMockGraphInfo(xaxes, yaxes) { return { _fullLayout: { dragmode: 'zoom', _paperdiv: d3.select(getMockContainerTree()), - _has: Plots._hasPlotType + _has: Plots._hasPlotType, + _subplots: {xaxis: xaxes || [], yaxis: yaxes || []} }, _fullData: [], _context: { @@ -188,7 +189,7 @@ describe('ModeBar', function() { ['toggleSpikelines', 'hoverClosestCartesian', 'hoverCompareCartesian'] ]); - var gd = getMockGraphInfo(); + var gd = getMockGraphInfo(['x'], ['y']); gd._fullLayout._basePlotModules = [{ name: 'cartesian' }]; gd._fullLayout.xaxis = {fixedrange: false}; @@ -206,7 +207,7 @@ describe('ModeBar', function() { ['toggleSpikelines', 'hoverClosestCartesian', 'hoverCompareCartesian'] ]); - var gd = getMockGraphInfo(); + var gd = getMockGraphInfo(['x'], ['y']); gd._fullLayout._basePlotModules = [{ name: 'cartesian' }]; gd._fullLayout.xaxis = {fixedrange: false}; gd._fullData = [{ @@ -230,7 +231,7 @@ describe('ModeBar', function() { ['toggleSpikelines', 'hoverClosestCartesian', 'hoverCompareCartesian'] ]); - var gd = getMockGraphInfo(); + var gd = getMockGraphInfo(['x'], ['y']); gd._fullLayout._basePlotModules = [{ name: 'cartesian' }]; gd._fullLayout.xaxis = {fixedrange: false}; gd._fullData = [{ @@ -364,7 +365,7 @@ describe('ModeBar', function() { ['hoverClosestGl2d'] ]); - var gd = getMockGraphInfo(); + var gd = getMockGraphInfo(['x'], ['y']); gd._fullLayout._basePlotModules = [{ name: 'gl2d' }]; gd._fullLayout.xaxis = {fixedrange: false}; @@ -427,7 +428,7 @@ describe('ModeBar', function() { ['toggleHover'] ]); - var gd = getMockGraphInfo(); + var gd = getMockGraphInfo(['x'], ['y']); gd._fullData = [{ type: 'scatter', visible: true, @@ -569,7 +570,7 @@ describe('ModeBar', function() { // gives 11 buttons in 5 groups by default function setupGraphInfo() { - var gd = getMockGraphInfo(); + var gd = getMockGraphInfo(['x'], ['y']); gd._fullLayout._basePlotModules = [{ name: 'cartesian' }]; gd._fullLayout.xaxis = {fixedrange: false}; return gd; diff --git a/test/jasmine/tests/plot_interact_test.js b/test/jasmine/tests/plot_interact_test.js index bcddf542b33..29d9792b684 100644 --- a/test/jasmine/tests/plot_interact_test.js +++ b/test/jasmine/tests/plot_interact_test.js @@ -5,6 +5,7 @@ var Lib = require('@src/lib'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); +var failTest = require('../assets/fail_test'); // This suite is more of a test of the structure of interaction elements on // various plot types. Tests of actual mouse interactions on cartesian plots @@ -111,12 +112,13 @@ describe('Test plot structure', function() { return Plotly.relayout(gd, {xaxis: null, yaxis: null}); }).then(function() { expect(countScatterTraces()).toEqual(0); - expect(countSubplots()).toEqual(0); - expect(countClipPaths()).toEqual(0); - expect(countDraggers()).toEqual(0); - - done(); - }); + // we still make one empty cartesian subplot if no other subplots are described + expect(countSubplots()).toEqual(1); + expect(countClipPaths()).toEqual(4); + expect(countDraggers()).toEqual(1); + }) + .catch(failTest) + .then(done); }); it('should restore layout axes when they get deleted', function(done) { @@ -156,9 +158,9 @@ describe('Test plot structure', function() { expect(countSubplots()).toEqual(1); expect(gd.layout.xaxis.range).toBeCloseToArray([-4.79980, 74.48580], 4); expect(gd.layout.yaxis.range).toBeCloseToArray([-1.2662, 17.67023], 4); - - done(); - }); + }) + .catch(failTest) + .then(done); }); }); @@ -273,46 +275,88 @@ describe('Test plot structure', function() { .then(done); }); + function assertClassCount(container3, msg, classes) { + Object.keys(classes).forEach(function(cls) { + expect(container3.selectAll('.' + cls).size()) + .toBe(classes[cls], msg + ': ' + cls); + }); + } + it('should be removed of traces in sequence', function(done) { expect(countSubplots()).toEqual(4); assertHeatmapNodes(4); assertContourNodes(2); expect(countColorBars()).toEqual(1); + assertClassCount(gd._fullLayout._infolayer, 'initial', { + 'g-gtitle': 1, + 'g-xtitle': 1, + 'g-x2title': 1, + 'g-ytitle': 1, + 'g-y2title': 1 + }); Plotly.deleteTraces(gd, [0]).then(function() { - expect(countSubplots()).toEqual(4); - expect(countClipPaths()).toEqual(12); - expect(countDraggers()).toEqual(4); + expect(countSubplots()).toEqual(3); + expect(countClipPaths()).toEqual(11); + expect(countDraggers()).toEqual(3); assertHeatmapNodes(3); assertContourNodes(2); expect(countColorBars()).toEqual(0); + assertClassCount(gd._fullLayout._infolayer, '1 down', { + 'g-gtitle': 1, + 'g-xtitle': 1, + 'g-x2title': 1, + 'g-ytitle': 1, + 'g-y2title': 1 + }); return Plotly.deleteTraces(gd, [0]); }).then(function() { - expect(countSubplots()).toEqual(4); - expect(countClipPaths()).toEqual(12); - expect(countDraggers()).toEqual(4); + expect(countSubplots()).toEqual(2); + expect(countClipPaths()).toEqual(7); + expect(countDraggers()).toEqual(2); assertHeatmapNodes(2); assertContourNodes(2); expect(countColorBars()).toEqual(0); + assertClassCount(gd._fullLayout._infolayer, '2 down', { + 'g-gtitle': 1, + 'g-xtitle': 1, + 'g-x2title': 1, + 'g-ytitle': 0, + 'g-y2title': 1 + }); return Plotly.deleteTraces(gd, [0]); }).then(function() { - expect(countSubplots()).toEqual(4); - expect(countClipPaths()).toEqual(12); - expect(countDraggers()).toEqual(4); + expect(countSubplots()).toEqual(1); + expect(countClipPaths()).toEqual(4); + expect(countDraggers()).toEqual(1); assertHeatmapNodes(1); assertContourNodes(1); expect(countColorBars()).toEqual(0); + assertClassCount(gd._fullLayout._infolayer, '3 down', { + 'g-gtitle': 1, + 'g-xtitle': 0, + 'g-x2title': 1, + 'g-ytitle': 0, + 'g-y2title': 1 + }); return Plotly.deleteTraces(gd, [0]); }).then(function() { - expect(countSubplots()).toEqual(3); - expect(countClipPaths()).toEqual(11); - expect(countDraggers()).toEqual(3); + expect(countSubplots()).toEqual(1); + expect(countClipPaths()).toEqual(4); + expect(countDraggers()).toEqual(1); assertHeatmapNodes(0); assertContourNodes(0); expect(countColorBars()).toEqual(0); + assertClassCount(gd._fullLayout._infolayer, 'all gone', { + 'g-gtitle': 1, + 'g-xtitle': 1, + 'g-x2title': 0, + 'g-ytitle': 1, + 'g-y2title': 0 + }); var update = { xaxis: null, @@ -323,15 +367,22 @@ describe('Test plot structure', function() { return Plotly.relayout(gd, update); }).then(function() { - expect(countSubplots()).toEqual(0); - expect(countClipPaths()).toEqual(0); - expect(countDraggers()).toEqual(0); + expect(countSubplots()).toEqual(1); + expect(countClipPaths()).toEqual(4); + expect(countDraggers()).toEqual(1); assertHeatmapNodes(0); assertContourNodes(0); expect(countColorBars()).toEqual(0); - - done(); - }); + assertClassCount(gd._fullLayout._infolayer, 'cleared layout axes', { + 'g-gtitle': 1, + 'g-xtitle': 1, + 'g-x2title': 0, + 'g-ytitle': 1, + 'g-y2title': 0 + }); + }) + .catch(failTest) + .then(done); }); }); @@ -388,10 +439,10 @@ describe('Test plot structure', function() { Plotly.deleteTraces(gd, [0]).then(function() { expect(countPieTraces()).toEqual(0); - expect(countSubplots()).toEqual(0); - - done(); - }); + expect(countSubplots()).toEqual(1); + }) + .catch(failTest) + .then(done); }); it('should be able to be restyled to a bar chart and back', function(done) { @@ -409,9 +460,9 @@ describe('Test plot structure', function() { expect(countPieTraces()).toEqual(1); expect(countBarTraces()).toEqual(0); expect(countSubplots()).toEqual(0); - - done(); - }); + }) + .catch(failTest) + .then(done); }); }); @@ -522,9 +573,9 @@ describe('plot svg clip paths', function() { expect(cp.substring(0, 5)).toEqual('url(#'); expect(cp.substring(cp.length - 1)).toEqual(')'); }); - - done(); - }); + }) + .catch(failTest) + .then(done); }); it('should set clip path url to ids appended to window url', function(done) { @@ -550,7 +601,8 @@ describe('plot svg clip paths', function() { }); base.remove(); - done(); - }); + }) + .catch(failTest) + .then(done); }); }); diff --git a/test/jasmine/tests/plots_test.js b/test/jasmine/tests/plots_test.js index 9725a29556d..a290d30eb93 100644 --- a/test/jasmine/tests/plots_test.js +++ b/test/jasmine/tests/plots_test.js @@ -224,7 +224,7 @@ describe('Test Plots', function() { describe('Plots.supplyTraceDefaults', function() { var supplyTraceDefaults = Plots.supplyTraceDefaults, - layout = {}; + layout = {_subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']}}; var traceIn, traceOut; @@ -269,93 +269,6 @@ describe('Test Plots', function() { }); }); - describe('Plots.getSubplotIds', function() { - var getSubplotIds = Plots.getSubplotIds; - - it('returns scene ids in order', function() { - var layout = { - scene2: {}, - scene: {}, - scene3: {} - }; - - expect(getSubplotIds(layout, 'gl3d')) - .toEqual(['scene', 'scene2', 'scene3']); - - expect(getSubplotIds(layout, 'cartesian')) - .toEqual([]); - expect(getSubplotIds(layout, 'geo')) - .toEqual([]); - expect(getSubplotIds(layout, 'no-valid-subplot-type')) - .toEqual([]); - }); - - it('returns geo ids in order', function() { - var layout = { - geo2: {}, - geo: {}, - geo3: {} - }; - - expect(getSubplotIds(layout, 'geo')) - .toEqual(['geo', 'geo2', 'geo3']); - - expect(getSubplotIds(layout, 'cartesian')) - .toEqual([]); - expect(getSubplotIds(layout, 'gl3d')) - .toEqual([]); - expect(getSubplotIds(layout, 'no-valid-subplot-type')) - .toEqual([]); - }); - - it('returns cartesian ids', function() { - var layout = { - _has: Plots._hasPlotType, - _plots: { xy: {}, x2y2: {} } - }; - - expect(getSubplotIds(layout, 'cartesian')) - .toEqual([]); - - layout._basePlotModules = [{ name: 'cartesian' }]; - expect(getSubplotIds(layout, 'cartesian')) - .toEqual(['xy', 'x2y2']); - expect(getSubplotIds(layout, 'gl2d')) - .toEqual([]); - - layout._basePlotModules = [{ name: 'gl2d' }]; - expect(getSubplotIds(layout, 'gl2d')) - .toEqual(['xy', 'x2y2']); - expect(getSubplotIds(layout, 'cartesian')) - .toEqual([]); - - }); - }); - - describe('Plots.findSubplotIds', function() { - var findSubplotIds = Plots.findSubplotIds; - var ids; - - it('should return subplots ids found in the data', function() { - var data = [{ - type: 'scatter3d', - scene: 'scene' - }, { - type: 'surface', - scene: 'scene2' - }, { - type: 'choropleth', - geo: 'geo' - }]; - - ids = findSubplotIds(data, 'geo'); - expect(ids).toEqual(['geo']); - - ids = findSubplotIds(data, 'gl3d'); - expect(ids).toEqual(['scene', 'scene2']); - }); - }); - describe('Plots.resize', function() { var gd; @@ -596,7 +509,9 @@ describe('Test Plots', function() { }); }); - describe('Plots.getSubplotCalcData', function() { + describe('getSubplotCalcData', function() { + var getSubplotCalcData = require('@src/plots/get_data').getSubplotCalcData; + var trace0 = { geo: 'geo2' }; var trace1 = { subplot: 'ternary10' }; var trace2 = { subplot: 'ternary10' }; @@ -608,22 +523,22 @@ describe('Test Plots', function() { ]; it('should extract calcdata traces associated with subplot (1)', function() { - var out = Plots.getSubplotCalcData(cd, 'geo', 'geo2'); + var out = getSubplotCalcData(cd, 'geo', 'geo2'); expect(out).toEqual([[{ trace: trace0 }]]); }); it('should extract calcdata traces associated with subplot (2)', function() { - var out = Plots.getSubplotCalcData(cd, 'ternary', 'ternary10'); + var out = getSubplotCalcData(cd, 'ternary', 'ternary10'); expect(out).toEqual([[{ trace: trace1 }], [{ trace: trace2 }]]); }); it('should return [] when no calcdata traces where found', function() { - var out = Plots.getSubplotCalcData(cd, 'geo', 'geo'); + var out = getSubplotCalcData(cd, 'geo', 'geo'); expect(out).toEqual([]); }); it('should return [] when subplot type is invalid', function() { - var out = Plots.getSubplotCalcData(cd, 'non-sense', 'geo2'); + var out = getSubplotCalcData(cd, 'non-sense', 'geo2'); expect(out).toEqual([]); }); }); @@ -756,4 +671,134 @@ describe('Test Plots', function() { }); }); }); + + describe('subplot cleaning logic', function() { + var gd; + + beforeEach(function() { gd = createGraphDiv(); }); + + afterEach(destroyGraphDiv); + + function assertCartesian(subplotsSVG, subplotsGL2D, msg) { + var subplotsAll = subplotsSVG.concat(subplotsGL2D); + var subplots3 = d3.select(gd).selectAll('.cartesianlayer .subplot'); + expect(subplots3.size()).toBe(subplotsAll.length, msg); + + subplotsAll.forEach(function(subplot) { + expect(d3.select(gd).selectAll('.cartesianlayer .subplot.' + subplot).size()) + .toBe(1, msg + ' - ' + subplot); + }); + + subplotsSVG.forEach(function(subplot) { + expect((gd._fullLayout._plots[subplot] || {})._scene2d) + .toBeUndefined(msg + ' - cartesian ' + subplot); + }); + + subplotsGL2D.forEach(function(subplot) { + expect((gd._fullLayout._plots[subplot] || {})._scene2d) + .toBeDefined(msg + ' - gl2d ' + subplot); + }); + } + + var subplotSelectors = { + gl3d: '.gl-container>div[id^="scene"]', + geo: '.geolayer>g', + mapbox: '.mapboxgl-map', + parcoords: '.parcoords-line-layers', + pie: '.pielayer .trace', + sankey: '.sankey', + ternary: '.ternarylayer>g' + }; + + function assertSubplot(type, n, msg) { + expect(d3.select(gd).selectAll(subplotSelectors[type]).size()) + .toBe(n, msg + ' - ' + type); + } + + // opts.cartesian and opts.gl2d should be arrays of subplot ids ('xy', 'x2y2' etc) + // others should be counts: gl3d, geo, mapbox, parcoords, pie, ternary + // if omitted, that subplot type is assumed to not exist + function assertSubplots(opts, msg) { + msg = msg || ''; + assertCartesian(opts.cartesian || [], opts.gl2d || [], msg); + Object.keys(subplotSelectors).forEach(function(type) { + assertSubplot(type, opts[type] || 0, msg); + }); + } + + var jsLogo = 'https://images.plot.ly/language-icons/api-home/js-logo.png'; + + it('makes at least a blank cartesian subplot', function(done) { + Plotly.newPlot(gd, [], {}) + .then(function() { + assertSubplots({cartesian: ['xy']}, 'totally blank'); + }) + .catch(fail) + .then(done); + }); + + it('uses the first x & y axes it finds in making a blank cartesian subplot', function(done) { + Plotly.newPlot(gd, [], {xaxis3: {}, yaxis4: {}}) + .then(function() { + assertSubplots({cartesian: ['x3y4']}, 'blank with axis objects'); + }) + .catch(fail) + .then(done); + }); + + it('shows expected cartesian subplots from visible traces and components', function(done) { + Plotly.newPlot(gd, [ + {y: [1, 2]} + ], { + // strange case: x2 is anchored to y2, so we show y2 + // even though no trace or component references it, only x2 + annotations: [{xref: 'x2', yref: 'paper'}], + xaxis2: {anchor: 'y2'}, + images: [{xref: 'x3', yref: 'y3', source: jsLogo}], + shapes: [{xref: 'x5', yref: 'y5'}] + }) + .then(function() { + assertSubplots({cartesian: ['xy', 'x2y2', 'x3y3', 'x5y5']}, 'visible components'); + }) + .catch(fail) + .then(done); + }); + + it('shows expected cartesian subplots from invisible traces and components', function(done) { + Plotly.newPlot(gd, [ + {y: [1, 2], visible: false} + ], { + // strange case: x2 is anchored to y2, so we show y2 + // even though no trace or component references it, only x2 + annotations: [{xref: 'x2', yref: 'paper', visible: false}], + xaxis2: {anchor: 'y2'}, + images: [{xref: 'x3', yref: 'y3', source: jsLogo, visible: false}], + shapes: [{xref: 'x5', yref: 'y5', visible: false}] + }) + .then(function() { + assertSubplots({cartesian: ['xy', 'x2y2', 'x3y3', 'x5y5']}, 'invisible components'); + }) + .catch(fail) + .then(done); + }); + + it('ignores unused axis and subplot objects', function(done) { + Plotly.plot('graph', [{ + type: 'pie', + values: [1] + }], { + xaxis: {}, + yaxis: {}, + scene: {}, + geo: {}, + ternary: {}, + mapbox: {} + }) + .then(function() { + assertSubplots({pie: 1}, 'just pie'); + }) + .catch(fail) + .then(done); + }); + }); }); diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index 3cdfa6a2237..94ca4160da6 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -577,6 +577,11 @@ describe('the range slider', function() { it('should default to *true* when range slider is visible', function() { var mock = { + data: [ + {y: [1, 2]}, + {y: [1, 2], yaxis: 'y2'}, + {y: [1, 2], yaxis: 'y3'} + ], layout: { xaxis: { rangeslider: {} }, yaxis: { anchor: 'x' }, @@ -595,6 +600,11 @@ describe('the range slider', function() { it('should honor user settings', function() { var mock = { + data: [ + {y: [1, 2]}, + {y: [1, 2], yaxis: 'y2'}, + {y: [1, 2], yaxis: 'y3'} + ], layout: { xaxis: { rangeslider: {} }, yaxis: { anchor: 'x', fixedrange: false }, diff --git a/test/jasmine/tests/shapes_test.js b/test/jasmine/tests/shapes_test.js index 10675a5b6ff..fb9e1247dd3 100644 --- a/test/jasmine/tests/shapes_test.js +++ b/test/jasmine/tests/shapes_test.js @@ -60,7 +60,8 @@ describe('Test shapes defaults:', function() { xaxis: {type: 'linear', range: [0, 20]}, yaxis: {type: 'log', range: [1, 5]}, xaxis2: {type: 'date', range: ['2006-06-05', '2006-06-09']}, - yaxis2: {type: 'category', range: [-0.5, 7.5]} + yaxis2: {type: 'category', range: [-0.5, 7.5]}, + _subplots: {xaxis: ['x', 'x2'], yaxis: ['y', 'y2']} }; Axes.setConvert(fullLayout.xaxis); diff --git a/test/jasmine/tests/ternary_test.js b/test/jasmine/tests/ternary_test.js index 5715e48499a..ebed92f9dd0 100644 --- a/test/jasmine/tests/ternary_test.js +++ b/test/jasmine/tests/ternary_test.js @@ -378,7 +378,8 @@ describe('ternary defaults', function() { beforeEach(function() { layoutOut = { - font: { color: 'red' } + font: { color: 'red' }, + _subplots: {ternary: ['ternary']} }; // needs a ternary-ref in a trace in order to be detected diff --git a/test/jasmine/tests/toimage_test.js b/test/jasmine/tests/toimage_test.js index ad9b3c5e159..f419504d03d 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(53660, 1e3, 'png image length'); + expect(d.length).toBeWithin(54660, 3e3, 'png image length'); }) .then(function() { return Plotly.toImage(gd, {format: 'jpeg', imageDataOnly: true}); }) .then(function(d) { @@ -159,7 +159,7 @@ describe('Plotly.toImage', function() { .then(function() { return Plotly.toImage(gd, {format: 'svg', imageDataOnly: true}); }) .then(function(d) { expect(d.indexOf('data:image/')).toBe(-1); - expect(d.length).toBeWithin(39485, 1e3, 'svg image length'); + expect(d.length).toBeWithin(32062, 1e3, 'svg image length'); }) .then(function() { return Plotly.toImage(gd, {format: 'webp', imageDataOnly: true}); }) .then(function(d) { diff --git a/test/jasmine/tests/transform_filter_test.js b/test/jasmine/tests/transform_filter_test.js index cc7034c739d..7dc565a1fad 100644 --- a/test/jasmine/tests/transform_filter_test.js +++ b/test/jasmine/tests/transform_filter_test.js @@ -14,7 +14,10 @@ var assertStyle = customAssertions.assertStyle; describe('filter transforms defaults:', function() { - var fullLayout = { _transformModules: [] }; + var fullLayout = { + _transformModules: [], + _subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']} + }; var traceIn, traceOut; diff --git a/test/jasmine/tests/transform_groupby_test.js b/test/jasmine/tests/transform_groupby_test.js index d55ab8bf58d..8a41e03db95 100644 --- a/test/jasmine/tests/transform_groupby_test.js +++ b/test/jasmine/tests/transform_groupby_test.js @@ -9,6 +9,15 @@ var customAssertions = require('../assets/custom_assertions'); var assertDims = customAssertions.assertDims; var assertStyle = customAssertions.assertStyle; + +function supplyDataDefaults(dataIn, dataOut) { + return Plots.supplyDataDefaults(dataIn, dataOut, {}, { + _subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']}, + _modules: [], + _basePlotModules: [] + }); +} + describe('groupby', function() { describe('one-to-many transforms:', function() { @@ -261,7 +270,7 @@ describe('groupby', function() { ] }]; - Plots.supplyDataDefaults(dataIn, dataOut, {}, {}); + supplyDataDefaults(dataIn, dataOut); for(var i = 0; i < dataOut.length; i++) { uniqueColors[dataOut[i].marker.color] = true; @@ -717,7 +726,7 @@ describe('groupby', function() { ] }]; - Plots.supplyDataDefaults(dataIn, dataOut, {}, {}); + supplyDataDefaults(dataIn, dataOut); for(var i = 0; i < dataOut.length; i++) { colors.push(dataOut[i].marker.color); diff --git a/test/jasmine/tests/transform_multi_test.js b/test/jasmine/tests/transform_multi_test.js index 829d84569db..d322679b8a4 100644 --- a/test/jasmine/tests/transform_multi_test.js +++ b/test/jasmine/tests/transform_multi_test.js @@ -12,10 +12,24 @@ var supplyAllDefaults = require('../assets/supply_defaults'); var assertDims = customAssertions.assertDims; var assertStyle = customAssertions.assertStyle; +var mockFullLayout = { + _subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']}, + _modules: [], + _basePlotModules: [], + _has: function() {}, + _dfltTitle: {x: 'xxx', y: 'yyy'} +}; + + describe('general transforms:', function() { 'use strict'; - var fullLayout = { _transformModules: [] }; + var fullLayout = { + _transformModules: [], + _subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']}, + _modules: [], + _basePlotModules: [] + }; var traceIn, traceOut; @@ -60,7 +74,7 @@ describe('general transforms:', function() { expect(traceOut.y).toBe(traceIn.y); }); - it('supplyTraceDefaults should honored global transforms', function() { + it('supplyTraceDefaults should honor global transforms', function() { traceIn = { y: [2, 1, 2], transforms: [{ @@ -75,7 +89,10 @@ describe('general transforms:', function() { _transformModules: [], _globalTransforms: [{ type: 'filter' - }] + }], + _subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']}, + _modules: [], + _basePlotModules: [] }; traceOut = Plots.supplyTraceDefaults(traceIn, 0, layout); @@ -117,7 +134,7 @@ describe('general transforms:', function() { }]; var dataOut = []; - Plots.supplyDataDefaults(dataIn, dataOut, {}, []); + Plots.supplyDataDefaults(dataIn, dataOut, {}, mockFullLayout); var msg; @@ -180,19 +197,25 @@ describe('user-defined transforms:', function() { var transformIn = { type: 'fake' }; var transformOut = {}; + var calledSupplyDefaults = false; + var calledTransform = false; + var calledSupplyLayoutDefaults = false; + var dataIn = [{ transforms: [transformIn] }]; - var fullData = [], - layout = {}, - fullLayout = { _has: function() {} }, - transitionData = {}; + var fullData = []; + var layout = {}; + var fullLayout = Lib.extendDeep({}, mockFullLayout); + var transitionData = {}; function assertSupplyDefaultsArgs(_transformIn, traceOut, _layout) { expect(_transformIn).toBe(transformIn); expect(_layout).toBe(fullLayout); + calledSupplyDefaults = true; + return transformOut; } @@ -203,6 +226,8 @@ describe('user-defined transforms:', function() { expect(opts.layout).toBe(layout); expect(opts.fullLayout).toBe(fullLayout); + calledTransform = true; + return dataOut; } @@ -211,6 +236,8 @@ describe('user-defined transforms:', function() { expect(_fullLayout).toBe(fullLayout); expect(_fullData).toBe(fullData); expect(_transitionData).toBe(transitionData); + + calledSupplyLayoutDefaults = true; } var fakeTransformModule = { @@ -226,6 +253,9 @@ describe('user-defined transforms:', function() { Plots.supplyDataDefaults(dataIn, fullData, layout, fullLayout); Plots.supplyLayoutModuleDefaults(layout, fullLayout, fullData, transitionData); delete Plots.transformsRegistry.fake; + expect(calledSupplyDefaults).toBe(true); + expect(calledTransform).toBe(true); + expect(calledSupplyLayoutDefaults).toBe(true); }); }); diff --git a/test/jasmine/tests/transform_sort_test.js b/test/jasmine/tests/transform_sort_test.js index 38e2be3b79d..8f2e37d080c 100644 --- a/test/jasmine/tests/transform_sort_test.js +++ b/test/jasmine/tests/transform_sort_test.js @@ -12,6 +12,11 @@ var supplyAllDefaults = require('../assets/supply_defaults'); describe('Test sort transform defaults:', function() { function _supply(trace, layout) { layout = layout || {}; + Lib.extendDeep(layout, { + _subplots: {cartesian: ['xy'], xaxis: ['x'], yaxis: ['y']}, + _modules: [], + _basePlotModules: [] + }); return Plots.supplyTraceDefaults(trace, 0, layout); } diff --git a/test/jasmine/tests/validate_test.js b/test/jasmine/tests/validate_test.js index d5597f23322..3014e3540a3 100644 --- a/test/jasmine/tests/validate_test.js +++ b/test/jasmine/tests/validate_test.js @@ -177,7 +177,11 @@ describe('Plotly.validate', function() { }); it('should work with isLinkedToArray attributes', function() { - var out = Plotly.validate([], { + var out = Plotly.validate([ + {y: [1, 2]}, + {y: [1, 2], xaxis: 'x2'}, + {y: [1, 2], xaxis: 'x3'} + ], { annotations: [{ text: 'some text' }, { @@ -394,7 +398,10 @@ describe('Plotly.validate', function() { }); it('should catch input errors for attribute with dynamic defaults', function() { - var out = Plotly.validate([], { + var out = Plotly.validate([ + {y: [1, 2]}, + {y: [1, 2], xaxis: 'x2', yaxis: 'y2'} + ], { xaxis: { constrain: 'domain', constraintoward: 'bottom'