diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index 9806bbbceb1..7b8fc4c1766 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -670,6 +670,7 @@ exports.doAutoRangeAndConstraints = function(gd) { var fullLayout = gd._fullLayout; var axList = Axes.list(gd, '', true); var matchGroups = fullLayout._axisMatchGroups || []; + var axLookup = {}; var ax; var axRng; @@ -677,6 +678,7 @@ exports.doAutoRangeAndConstraints = function(gd) { ax = axList[i]; cleanAxisConstraints(gd, ax); doAutoRange(gd, ax); + axLookup[ax._id] = 1; } enforceAxisConstraints(gd); @@ -689,6 +691,10 @@ exports.doAutoRangeAndConstraints = function(gd) { for(id in group) { ax = Axes.getFromId(gd, id); + + // skip over 'missing' axes which do not pass through doAutoRange + if(!axLookup[ax._id]) continue; + // if one axis has autorange false, we're done if(ax.autorange === false) continue groupLoop; axRng = Lib.simpleMap(ax.range, ax.r2l); diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 7bbaf033341..af04c97d90b 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -1669,10 +1669,14 @@ axes.drawOne = function(gd, ax, opts) { var axId = ax._id; var axLetter = axId.charAt(0); var counterLetter = axes.counterLetter(axId); - var mainLinePosition = ax._mainLinePosition; - var mainMirrorPosition = ax._mainMirrorPosition; var mainPlotinfo = fullLayout._plots[ax._mainSubplot]; + + // this happens when updating matched group with 'missing' axes + if(!mainPlotinfo) return; + var mainAxLayer = mainPlotinfo[axLetter + 'axislayer']; + var mainLinePosition = ax._mainLinePosition; + var mainMirrorPosition = ax._mainMirrorPosition; var vals = ax._vals = axes.calcTicks(ax); diff --git a/src/plots/cartesian/constants.js b/src/plots/cartesian/constants.js index ed1b0229a2b..a06eff59894 100644 --- a/src/plots/cartesian/constants.js +++ b/src/plots/cartesian/constants.js @@ -7,11 +7,10 @@ */ 'use strict'; -var counterRegex = require('../../lib/regex').counter; +var counterRegex = require('../../lib/regex').counter; module.exports = { - idRegex: { x: counterRegex('x'), y: counterRegex('y') diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index 84d518dfc27..21c651c245e 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -24,6 +24,8 @@ var axisIds = require('./axis_ids'); var id2name = axisIds.id2name; var name2id = axisIds.name2id; +var AX_ID_PATTERN = require('./constants').AX_ID_PATTERN; + var Registry = require('../../registry'); var traceIs = Registry.traceIs; var getComponentMethod = Registry.getComponentMethod; @@ -133,7 +135,28 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { var bgColor = Color.combine(plotBgColor, layoutOut.paper_bgcolor); - var axName, axLetter, axLayoutIn, axLayoutOut; + // name of single axis (e.g. 'xaxis', 'yaxis2') + var axName; + // id of single axis (e.g. 'y', 'x5') + var axId; + // 'x' or 'y' + var axLetter; + // input layout axis container + var axLayoutIn; + // full layout axis container + var axLayoutOut; + + function newAxLayoutOut() { + var traces = ax2traces[axName] || []; + axLayoutOut._traceIndices = traces.map(function(t) { return t._expandedIndex; }); + axLayoutOut._annIndices = []; + axLayoutOut._shapeIndices = []; + axLayoutOut._imgIndices = []; + axLayoutOut._subplotsWith = []; + axLayoutOut._counterAxes = []; + axLayoutOut._name = axLayoutOut._attr = axName; + axLayoutOut._id = axId; + } function coerce(attr, dflt) { return Lib.coerce(axLayoutIn, axLayoutOut, layoutAttributes, attr, dflt); @@ -147,9 +170,6 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { return (axLetter === 'x') ? yIds : xIds; } - var counterAxes = {x: getCounterAxes('x'), y: getCounterAxes('y')}; - var allAxisIds = counterAxes.x.concat(counterAxes.y); - function getOverlayableAxes(axLetter, axName) { var list = (axLetter === 'x') ? xNames : yNames; var out = []; @@ -165,9 +185,30 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { return out; } + // list of available counter axis names + var counterAxes = {x: getCounterAxes('x'), y: getCounterAxes('y')}; + // list of all x AND y axis ids + var allAxisIds = counterAxes.x.concat(counterAxes.y); + // lookup and list of axis ids that axes in axNames have a reference to, + // even though they are missing from allAxisIds + var missingMatchedAxisIdsLookup = {}; + var missingMatchedAxisIds = []; + + // fill in 'missing' axis lookup when an axis is set to match an axis + // not part of the allAxisIds list, save axis type so that we can propagate + // it to the missing axes + function addMissingMatchedAxis() { + var matchesIn = axLayoutIn.matches; + if(AX_ID_PATTERN.test(matchesIn) && allAxisIds.indexOf(matchesIn) === -1) { + missingMatchedAxisIdsLookup[matchesIn] = axLayoutIn.type; + missingMatchedAxisIds = Object.keys(missingMatchedAxisIdsLookup); + } + } + // first pass creates the containers, determines types, and handles most of the settings for(i = 0; i < axNames.length; i++) { axName = axNames[i]; + axId = name2id(axName); axLetter = axName.charAt(0); if(!Lib.isPlainObject(layoutIn[axName])) { @@ -176,20 +217,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { axLayoutIn = layoutIn[axName]; axLayoutOut = Template.newContainer(layoutOut, axName, axLetter + 'axis'); - - var traces = ax2traces[axName] || []; - axLayoutOut._traceIndices = traces.map(function(t) { return t._expandedIndex; }); - axLayoutOut._annIndices = []; - axLayoutOut._shapeIndices = []; - axLayoutOut._imgIndices = []; - axLayoutOut._subplotsWith = []; - axLayoutOut._counterAxes = []; - - // set up some private properties - axLayoutOut._name = axLayoutOut._attr = axName; - var id = axLayoutOut._id = name2id(axName); - - var overlayableAxes = getOverlayableAxes(axLetter, axName); + newAxLayoutOut(); var visibleDflt = (axLetter === 'x' && !xaMustDisplay[axName] && xaMayHide[axName]) || @@ -207,13 +235,13 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { font: layoutOut.font, outerTicks: outerTicks[axName], showGrid: !noGrids[axName], - data: traces, + data: ax2traces[axName] || [], bgColor: bgColor, calendar: layoutOut.calendar, automargin: true, visibleDflt: visibleDflt, reverseDflt: reverseDflt, - splomStash: ((layoutOut._splomAxes || {})[axLetter] || {})[id] + splomStash: ((layoutOut._splomAxes || {})[axLetter] || {})[axId] }; coerce('uirevision', layoutOut.uirevision); @@ -239,12 +267,63 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { handlePositionDefaults(axLayoutIn, axLayoutOut, coerce, { letter: axLetter, counterAxes: counterAxes[axLetter], - overlayableAxes: overlayableAxes, + overlayableAxes: getOverlayableAxes(axLetter, axName), grid: layoutOut.grid }); coerce('title.standoff'); + addMissingMatchedAxis(); + + axLayoutOut._input = axLayoutIn; + } + + // coerce the 'missing' axes + i = 0; + while(i < missingMatchedAxisIds.length) { + axId = missingMatchedAxisIds[i++]; + axName = id2name(axId); + axLetter = axName.charAt(0); + + if(!Lib.isPlainObject(layoutIn[axName])) { + layoutIn[axName] = {}; + } + + axLayoutIn = layoutIn[axName]; + axLayoutOut = Template.newContainer(layoutOut, axName, axLetter + 'axis'); + newAxLayoutOut(); + + var defaultOptions2 = { + letter: axLetter, + font: layoutOut.font, + outerTicks: outerTicks[axName], + showGrid: !noGrids[axName], + data: [], + bgColor: bgColor, + calendar: layoutOut.calendar, + automargin: true, + visibleDflt: false, + reverseDflt: false, + splomStash: ((layoutOut._splomAxes || {})[axLetter] || {})[axId] + }; + + coerce('uirevision', layoutOut.uirevision); + + axLayoutOut.type = missingMatchedAxisIdsLookup[axId] || 'linear'; + + handleAxisDefaults(axLayoutIn, axLayoutOut, coerce, defaultOptions2, layoutOut); + + handlePositionDefaults(axLayoutIn, axLayoutOut, coerce, { + letter: axLetter, + counterAxes: counterAxes[axLetter], + overlayableAxes: getOverlayableAxes(axLetter, axName), + grid: layoutOut.grid + }); + + coerce('fixedrange'); + + addMissingMatchedAxis(); + axLayoutOut._input = axLayoutIn; } @@ -295,9 +374,12 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { var constraintGroups = layoutOut._axisConstraintGroups = []; // similar to _axisConstraintGroups, but for matching axes var matchGroups = layoutOut._axisMatchGroups = []; + // make sure to include 'missing' axes here + var allAxisIdsIncludingMissing = allAxisIds.concat(missingMatchedAxisIds); + var axNamesIncludingMissing = axNames.concat(Lib.simpleMap(missingMatchedAxisIds, id2name)); - for(i = 0; i < axNames.length; i++) { - axName = axNames[i]; + for(i = 0; i < axNamesIncludingMissing.length; i++) { + axName = axNamesIncludingMissing[i]; axLetter = axName.charAt(0); axLayoutIn = layoutIn[axName]; axLayoutOut = layoutOut[axName]; @@ -305,15 +387,19 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { var scaleanchorDflt; if(axLetter === 'y' && !axLayoutIn.hasOwnProperty('scaleanchor') && axHasImage[axName]) { scaleanchorDflt = axLayoutOut.anchor; - } else {scaleanchorDflt = undefined;} + } else { + scaleanchorDflt = undefined; + } var constrainDflt; if(!axLayoutIn.hasOwnProperty('constrain') && axHasImage[axName]) { constrainDflt = 'domain'; - } else {constrainDflt = undefined;} + } else { + constrainDflt = undefined; + } handleConstraintDefaults(axLayoutIn, axLayoutOut, coerce, { - allAxisIds: allAxisIds, + allAxisIds: allAxisIdsIncludingMissing, layoutOut: layoutOut, scaleanchorDflt: scaleanchorDflt, constrainDflt: constrainDflt @@ -324,7 +410,6 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { var group = matchGroups[i]; var rng = null; var autorange = null; - var axId; // find 'matching' range attrs for(axId in group) { diff --git a/src/plots/plots.js b/src/plots/plots.js index 4ec29c19aa8..55fd49714ca 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -1272,10 +1272,13 @@ plots.supplyTraceDefaults = function(traceIn, traceOut, colorIndex, layout, trac var subplots = layout._subplots; 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( + visible || + basePlotModule.name !== 'gl2d' // for now just drop empty gl2d subplots + // 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? + ) { if(Array.isArray(subplotAttr)) { for(i = 0; i < subplotAttr.length; i++) { var attri = subplotAttr[i]; @@ -2934,7 +2937,7 @@ plots.doCalcdata = function(gd, traces) { calcdata[i] = cd; } - setupAxisCategories(axList, fullData); + setupAxisCategories(axList, fullData, fullLayout); // 'transform' loop - must calc container traces first // so that if their dependent traces can get transform properly @@ -2942,7 +2945,7 @@ plots.doCalcdata = function(gd, traces) { for(i = 0; i < fullData.length; i++) transformCalci(i); // clear stuff that should recomputed in 'regular' loop - if(hasCalcTransform) setupAxisCategories(axList, fullData); + if(hasCalcTransform) setupAxisCategories(axList, fullData, fullLayout); // 'regular' loop - make sure container traces (eg carpet) calc before // contained traces (eg contourcarpet) @@ -3147,13 +3150,31 @@ function sortAxisCategoriesByValue(axList, gd) { return affectedTraces; } -function setupAxisCategories(axList, fullData) { - for(var i = 0; i < axList.length; i++) { - var ax = axList[i]; +function setupAxisCategories(axList, fullData, fullLayout) { + var axLookup = {}; + var i, ax, axId; + + for(i = 0; i < axList.length; i++) { + ax = axList[i]; + axId = ax._id; + ax.clearCalc(); if(ax.type === 'multicategory') { ax.setupMultiCategory(fullData); } + + axLookup[ax._id] = 1; + } + + // look into match groups for 'missing' axes + var matchGroups = fullLayout._axisMatchGroups || []; + for(i = 0; i < matchGroups.length; i++) { + for(axId in matchGroups[i]) { + if(!axLookup[axId]) { + ax = fullLayout[axisIDs.id2name(axId)]; + ax.clearCalc(); + } + } } } diff --git a/test/image/baselines/matching-missing-axes.png b/test/image/baselines/matching-missing-axes.png new file mode 100644 index 00000000000..2d067c0a1cd Binary files /dev/null and b/test/image/baselines/matching-missing-axes.png differ diff --git a/test/image/compare_pixels_test.js b/test/image/compare_pixels_test.js index 07848a385ad..3b08dcbdf20 100644 --- a/test/image/compare_pixels_test.js +++ b/test/image/compare_pixels_test.js @@ -101,9 +101,8 @@ if(allMock || argv.filter) { } var FLAKY_LIST = [ - 'treemap_coffee', 'treemap_textposition', - 'treemap_with-without_values_template', + 'treemap_with-without_values', 'trace_metatext', 'gl3d_directions-streamtube1' ]; diff --git a/test/image/mocks/matching-missing-axes.json b/test/image/mocks/matching-missing-axes.json new file mode 100644 index 00000000000..0a11637a56a --- /dev/null +++ b/test/image/mocks/matching-missing-axes.json @@ -0,0 +1,317 @@ +{ + "data": [ + { + "type": "bar", + "name": "smoker=No", + "alignmentgroup": "True", + "offsetgroup": "smoker=No", + "legendgroup": "smoker=No", + "showlegend": true, + "marker": { "color": "#636efa" }, + "x": [ "Female", "Male", "Male", "Male", "Female", "Male", "Male", "Male", "Male", "Male", "Male", "Female", "Male", "Male", "Female", "Male", "Female", "Male", "Female", "Male", "Male", "Male", "Male", "Male", "Male", "Male", "Male", "Male", "Male", "Female", "Female", "Male", "Male", "Male", "Male", "Male", "Female", "Female", "Male", "Male", "Male", "Male", "Male", "Male", "Female", "Male", "Female", "Female", "Male", "Male", "Male", "Female", "Male", "Male", "Male", "Male", "Male" ], + "y": [ 16.99, 10.34, 21.01, 23.68, 24.59, 25.29, 8.77, 26.88, 15.04, 14.78, 10.27, 35.26, 15.42, 18.43, 14.83, 21.58, 10.33, 16.29, 16.97, 17.46, 13.94, 9.68, 30.4, 18.29, 22.23, 32.4, 28.55, 18.04, 12.54, 10.29, 34.81, 9.94, 25.56, 19.49, 38.07, 23.95, 25.71, 17.31, 29.93, 14.07, 13.13, 17.26, 24.55, 19.77, 29.85, 48.17, 25, 13.39, 16.49, 21.5, 12.66, 16.21, 13.81, 24.52, 20.76, 31.71, 20.69 ], + "xaxis": "x5", + "yaxis": "y5" + }, + { + "type": "bar", + "name": "smoker=No", + "alignmentgroup": "True", + "offsetgroup": "smoker=No", + "legendgroup": "smoker=No", + "showlegend": false, + "marker": { "color": "#636efa" }, + "x": [ "Male", "Male", "Female", "Female", "Male", "Male", "Male", "Male", "Male", "Male", "Female", "Male", "Male", "Female", "Female", "Male", "Male", "Male", "Female", "Male", "Male", "Male", "Female", "Male", "Male", "Male", "Female", "Male", "Male", "Female", "Female", "Male", "Female", "Male", "Male", "Female", "Male", "Male", "Male", "Male", "Male", "Male", "Female", "Male", "Male" ], + "y": [ 20.65, 17.92, 20.29, 15.77, 39.42, 19.82, 17.81, 13.37, 12.69, 21.7, 19.65, 9.55, 18.35, 15.06, 20.69, 17.78, 24.06, 16.31, 16.93, 18.69, 31.27, 16.04, 26.41, 48.27, 17.59, 20.08, 16.45, 20.23, 12.02, 17.07, 14.73, 10.51, 20.92, 18.24, 14, 7.25, 48.33, 20.45, 13.28, 11.61, 10.77, 10.07, 35.83, 29.03, 17.82 ], + "xaxis": "x6", + "yaxis": "y6" + }, + { + "type": "bar", + "name": "smoker=No", + "alignmentgroup": "True", + "offsetgroup": "smoker=No", + "legendgroup": "smoker=No", + "showlegend": false, + "marker": { "color": "#636efa" }, + "x": [ "Female" ], + "y": [ 18.78 ], + "xaxis": "x7", + "yaxis": "y7" + }, + { + "type": "bar", + "name": "smoker=No", + "alignmentgroup": "True", + "offsetgroup": "smoker=No", + "legendgroup": "smoker=No", + "showlegend": false, + "marker": { "color": "#636efa" }, + "x": [ "Male", "Female", "Male" ], + "y": [ 22.49, 22.75, 12.46 ], + "xaxis": "x8", + "yaxis": "y8", + "showlegend": false + }, + { + "type": "bar", + "name": "smoker=No", + "alignmentgroup": "True", + "offsetgroup": "smoker=No", + "legendgroup": "smoker=No", + "showlegend": false, + "marker": { "color": "#636efa" }, + "x": [ "Male", "Male", "Male", "Male", "Female", "Male", "Female", "Male", "Male", "Male", "Male", "Female", "Female", "Female", "Male", "Female", "Male", "Male", "Female", "Female", "Male", "Female", "Female", "Male", "Male", "Female", "Female", "Female", "Female", "Female", "Female", "Female", "Female", "Female", "Male", "Male", "Female", "Female", "Female", "Female", "Female", "Male", "Male", "Male" ], + "y": [ 27.2, 22.76, 17.29, 16.66, 10.07, 15.98, 34.83, 13.03, 18.28, 24.71, 21.16, 10.65, 12.43, 24.08, 11.69, 13.42, 14.26, 15.95, 12.48, 29.8, 8.52, 14.52, 11.38, 22.82, 19.08, 20.27, 11.17, 12.26, 18.26, 8.51, 10.33, 14.15, 13.16, 17.47, 34.3, 41.19, 27.05, 16.43, 8.35, 18.64, 11.87, 9.78, 7.51, 7.56 ], + "xaxis": "x3", + "yaxis": "y3" + }, + { + "type": "bar", + "name": "smoker=No", + "alignmentgroup": "True", + "offsetgroup": "smoker=No", + "legendgroup": "smoker=No", + "showlegend": false, + "marker": { "color": "#636efa" }, + "x": [ "Female" ], + "y": [ 15.98 ], + "xaxis": "x4", + "yaxis": "y4" + }, + { + "type": "bar", + "name": "smoker=Yes", + "alignmentgroup": "True", + "offsetgroup": "smoker=Yes", + "legendgroup": "smoker=Yes", + "showlegend": true, + "marker": { "color": "#EF553B" }, + "x": [ "Female", "Male", "Male", "Male", "Male", "Male", "Male", "Female", "Male", "Male", "Male", "Male", "Male", "Male", "Female", "Male", "Female", "Male", "Male" ], + "y": [ 17.51, 7.25, 31.85, 16.82, 32.9, 17.89, 14.48, 9.6, 34.63, 34.65, 23.33, 45.35, 23.17, 40.55, 20.9, 30.46, 18.15, 23.1, 15.69 ], + "xaxis": "x5", + "yaxis": "y5" + }, + { + "type": "bar", + "name": "smoker=Yes", + "alignmentgroup": "True", + "offsetgroup": "smoker=Yes", + "legendgroup": "smoker=Yes", + "showlegend": false, + "marker": { "color": "#EF553B" }, + "x": [ "Male", "Male", "Male", "Male", "Male", "Male", "Female", "Male", "Female", "Female", "Male", "Female", "Female", "Male", "Male", "Male", "Female", "Female", "Female", "Male", "Male", "Male", "Male", "Male", "Female", "Male", "Male", "Female", "Female", "Female", "Male", "Male", "Male", "Female", "Female", "Male", "Male", "Male", "Male", "Male", "Female", "Male" ], + "y": [ 38.01, 11.24, 20.29, 13.81, 11.02, 18.29, 3.07, 15.01, 26.86, 25.28, 17.92, 44.3, 22.42, 15.36, 20.49, 25.21, 14.31, 10.59, 10.63, 50.81, 15.81, 26.59, 38.73, 24.27, 12.76, 30.06, 25.89, 13.27, 28.17, 12.9, 28.15, 11.59, 7.74, 30.14, 22.12, 24.01, 15.69, 15.53, 12.6, 32.83, 27.18, 22.67 ], + "xaxis": "x6", + "yaxis": "y6" + }, + { + "type": "bar", + "name": "smoker=Yes", + "alignmentgroup": "True", + "offsetgroup": "smoker=Yes", + "legendgroup": "smoker=Yes", + "showlegend": false, + "marker": { "color": "#EF553B" }, + "x": [ "Male", "Female", "Female", "Male", "Male", "Male", "Male", "Female", "Female" ], + "y": [ 28.97, 5.75, 16.32, 40.17, 27.28, 12.03, 21.01, 11.35, 15.38 ], + "xaxis": "x8", + "yaxis": "y8" + }, + { + "type": "bar", + "name": "smoker=Yes", + "alignmentgroup": "True", + "offsetgroup": "smoker=Yes", + "legendgroup": "smoker=Yes", + "showlegend": false, + "marker": { "color": "#EF553B" }, + "x": [ "Male", "Male", "Male", "Female", "Male", "Male", "Male", "Male", "Female", "Female", "Male", "Male", "Female", "Female", "Female", "Male", "Female" ], + "y": [ 19.44, 32.68, 16, 19.81, 28.44, 15.48, 16.58, 10.34, 43.11, 13, 13.51, 18.71, 12.74, 13, 16.4, 20.53, 16.47 ], + "xaxis": "x3", + "yaxis": "y3" + }, + { + "type": "bar", + "name": "smoker=Yes", + "alignmentgroup": "True", + "offsetgroup": "smoker=Yes", + "legendgroup": "smoker=Yes", + "showlegend": false, + "marker": { "color": "#EF553B" }, + "x": [ "Male", "Female", "Male", "Male", "Female", "Female" ], + "y": [ 12.16, 13.42, 8.58, 13.42, 16.27, 10.09 ], + "xaxis": "x4", + "yaxis": "y4" + } + ], + "layout": { + "annotations": [ + { + "showarrow": false, + "text": "day=Sun", + "x": 0.11499999999999999, + "xanchor": "center", + "xref": "paper", + "y": 1, + "yanchor": "bottom", + "yref": "paper" + }, + { + "showarrow": false, + "text": "day=Sat", + "x": 0.365, + "xanchor": "center", + "xref": "paper", + "y": 1, + "yanchor": "bottom", + "yref": "paper" + }, + { + "showarrow": false, + "text": "day=Thur", + "x": 0.615, + "xanchor": "center", + "xref": "paper", + "y": 1, + "yanchor": "bottom", + "yref": "paper" + }, + { + "showarrow": false, + "text": "day=Fri", + "x": 0.865, + "xanchor": "center", + "xref": "paper", + "y": 1, + "yanchor": "bottom", + "yref": "paper" + }, + { + "showarrow": false, + "text": "time=Lunch", + "textangle": 90, + "x": 0.98, + "xanchor": "left", + "xref": "paper", + "y": 0.2425, + "yanchor": "middle", + "yref": "paper" + }, + { + "showarrow": false, + "text": "time=Dinner", + "textangle": 90, + "x": 0.98, + "xanchor": "left", + "xref": "paper", + "y": 0.7575000000000001, + "yanchor": "middle", + "yref": "paper" + } + ], + "barmode": "group", + "legend": { "tracegroupgap": 0 }, + "margin": { "t": 60 }, + "xaxis": { + "anchor": "y", + "domain": [ 0, 0.22999999999999998 ], + "title": { "text": "sex" }, + "categoryarray": [ "Male", "Female" ] + }, + "xaxis2": { + "anchor": "y2", + "domain": [ 0.24999999999999997, 0.48 ], + "matches": "x", + "title": { "text": "sex" } + }, + "xaxis3": { + "anchor": "y3", + "domain": [ 0.49999999999999994, 0.73 ], + "matches": "x", + "title": { "text": "sex" } + }, + "xaxis4": { + "anchor": "y4", + "domain": [ 0.75, 0.98 ], + "matches": "x", + "title": { "text": "sex" } + }, + "xaxis5": { + "anchor": "y5", + "domain": [ 0, 0.22999999999999998 ], + "matches": "x", + "showticklabels": false + }, + "xaxis6": { + "anchor": "y6", + "domain": [ 0.24999999999999997, 0.48 ], + "matches": "x", + "showticklabels": false + }, + "xaxis7": { + "anchor": "y7", + "domain": [ 0.49999999999999994, 0.73 ], + "matches": "x", + "showticklabels": false + }, + "xaxis8": { + "anchor": "y8", + "domain": [ 0.75, 0.98 ], + "matches": "x", + "showticklabels": false + }, + "yaxis": { + "anchor": "x", + "domain": [ 0, 0.485 ], + "title": { "text": "total_bill" } + }, + "yaxis2": { + "anchor": "x2", + "domain": [ 0, 0.485 ], + "matches": "y", + "showticklabels": false + }, + "yaxis3": { + "anchor": "x3", + "domain": [ 0, 0.485 ], + "matches": "y", + "showticklabels": false + }, + "yaxis4": { + "anchor": "x4", + "domain": [ 0, 0.485 ], + "matches": "y", + "showticklabels": false + }, + "yaxis5": { + "anchor": "x5", + "domain": [ 0.515, 1 ], + "matches": "y", + "title": { "text": "total_bill" } + }, + "yaxis6": { + "anchor": "x6", + "domain": [ 0.515, 1 ], + "matches": "y", + "showticklabels": false + }, + "yaxis7": { + "anchor": "x7", + "domain": [ 0.515, 1 ], + "matches": "y", + "showticklabels": false + }, + "yaxis8": { + "anchor": "x8", + "domain": [ 0.515, 1 ], + "matches": "y", + "showticklabels": false + }, + "title": { + "text" : "Matching missing axes 'x' and 'y'
with categoryarray set in xaxis", + "x": 0.05, + "y": 0.2 + } + } +} diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index e51bd397ead..83a3298876f 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -830,6 +830,68 @@ describe('Test axes', function() { expect(layoutOut._axisMatchGroups).toContain({y: 1, y2: 1, y3: 1}); }); + it('should find matching group even when matching a *missing* axis', function() { + layoutIn = { + // N.B. xaxis isn't set + xaxis2: {matches: 'x'}, + xaxis3: {matches: 'x'}, + xaxis4: {matches: 'x'}, + // N.B. yaxis isn't set + yaxis2: {matches: 'y'}, + yaxis3: {matches: 'y2'}, + yaxis4: {matches: 'y3'}, + }; + layoutOut._subplots.cartesian = ['x2y2', 'x3y3', 'x4y4']; + layoutOut._subplots.xaxis = ['x2', 'x3', 'x4']; + layoutOut._subplots.yaxis = ['y2', 'y3', 'y4']; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + expect(layoutOut._axisMatchGroups.length).toBe(2); + expect(layoutOut._axisMatchGroups).toContain({x: 1, x2: 1, x3: 1, x4: 1}); + expect(layoutOut._axisMatchGroups).toContain({y: 1, y2: 1, y3: 1, y4: 1}); + + // should coerce the 'missing' axes + expect(layoutIn.xaxis).toBeDefined(); + expect(layoutIn.yaxis).toBeDefined(); + expect(layoutOut.xaxis).toBeDefined(); + expect(layoutOut.yaxis).toBeDefined(); + }); + + it('should find matching group even when matching a *missing* axis (nested case)', function() { + layoutIn = { + // N.B. xaxis isn't set + // N.B. xaxis2 is set, but does not correspond to a subplot + xaxis2: {matches: 'x'}, + xaxis3: {matches: 'x2'}, + xaxis4: {matches: 'x3'}, + // N.B. yaxis isn't set + // N.B yaxis2 does not correspond to a subplot and is useless here + yaxis2: {matches: 'y'}, + yaxis3: {matches: 'y'}, + yaxis4: {matches: 'y3'} + }; + layoutOut._subplots.cartesian = ['x3y3', 'x4y4']; + layoutOut._subplots.xaxis = ['x3', 'x4']; + layoutOut._subplots.yaxis = ['y3', 'y4']; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + expect(layoutOut._axisMatchGroups.length).toBe(2); + expect(layoutOut._axisMatchGroups).toContain({x: 1, x2: 1, x3: 1, x4: 1}); + expect(layoutOut._axisMatchGroups).toContain({y: 1, y3: 1, y4: 1}); + + // should coerce the 'missing' axes + expect(layoutIn.xaxis).toBeDefined(); + expect(layoutIn.yaxis).toBeDefined(); + expect(layoutOut.xaxis).toBeDefined(); + expect(layoutOut.yaxis).toBeDefined(); + + // should coerce useless axes + expect(layoutIn.yaxis2).toEqual({matches: 'y'}); + expect(layoutOut.yaxis2).toBeUndefined(); + }); + it('should match set axis range value for matching axes', function() { layoutIn = { // autorange case @@ -871,6 +933,72 @@ describe('Test axes', function() { _assertMatchingAxes(['xaxis4', 'yaxis4'], false, [-1, 3]); }); + it('should match set axis range value for matching axes even when matching a *missing* axis', function() { + layoutIn = { + // N.B. xaxis is set, but does not correspond to a subplot + xaxis: {range: [0, 1]}, + xaxis2: {matches: 'x'}, + xaxis4: {matches: 'x'} + }; + layoutOut._subplots.cartesian = ['x2y2', 'x4y4']; + layoutOut._subplots.xaxis = ['x2', 'x4']; + layoutOut._subplots.yaxis = ['y2', 'y4']; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + expect(layoutOut._axisMatchGroups.length).toBe(1); + expect(layoutOut._axisMatchGroups).toContain({x: 1, x2: 1, x4: 1}); + + expect(layoutOut.xaxis.range).withContext('xaxis.range').toEqual([0, 1]); + expect(layoutOut.xaxis2.range).withContext('xaxis2.range').toEqual([0, 1]); + expect(layoutOut.xaxis4.range).withContext('xaxis4.range').toEqual([0, 1]); + }); + + it('should match set axis range value for matching axes even when matching a *missing* axis (nested case)', function() { + layoutIn = { + // N.B. xaxis is set, but does not correspond to a subplot + xaxis: {range: [0, 1]}, + // N.B. xaxis2 is set, but does not correspond to a subplot + xaxis2: {matches: 'x'}, + xaxis3: {matches: 'x2'}, + xaxis4: {matches: 'x3'} + }; + layoutOut._subplots.cartesian = ['x3y3', 'x4y4']; + layoutOut._subplots.xaxis = ['x3', 'x4']; + layoutOut._subplots.yaxis = ['y3', 'y4']; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + expect(layoutOut._axisMatchGroups.length).toBe(1); + expect(layoutOut._axisMatchGroups).toContain({x: 1, x2: 1, x3: 1, x4: 1}); + + expect(layoutOut.xaxis.range).withContext('xaxis.range').toEqual([0, 1]); + expect(layoutOut.xaxis2.range).withContext('xaxis2.range').toEqual([0, 1]); + expect(layoutOut.xaxis2.range).withContext('xaxis3.range').toEqual([0, 1]); + expect(layoutOut.xaxis4.range).withContext('xaxis4.range').toEqual([0, 1]); + }); + + it('should propagate axis type into *missing* axes', function() { + layoutIn = { + xaxis2: {type: 'date', matches: 'x'}, + yaxis: {type: 'category', matches: 'y2'} + }; + layoutOut._subplots.cartesian = ['x2y']; + layoutOut._subplots.xaxis = ['x2']; + layoutOut._subplots.yaxis = ['y']; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + expect(layoutOut._axisMatchGroups.length).toBe(2); + expect(layoutOut._axisMatchGroups).toContain({x: 1, x2: 1}); + expect(layoutOut._axisMatchGroups).toContain({y: 1, y2: 1}); + + expect(layoutOut.xaxis.type).withContext('xaxis.type').toBe('date'); + expect(layoutOut.xaxis2.type).withContext('xaxis2.type').toBe('date'); + expect(layoutOut.yaxis.type).withContext('yaxis.type').toBe('category'); + expect(layoutOut.yaxis2.type).withContext('yaxis2.type').toBe('category'); + }); + it('should adapt default axis ranges to *rangemode*', function() { layoutIn = { xaxis: {rangemode: 'tozero'}, diff --git a/test/jasmine/tests/cartesian_interact_test.js b/test/jasmine/tests/cartesian_interact_test.js index 68ccfa0a4e5..29bb79810ef 100644 --- a/test/jasmine/tests/cartesian_interact_test.js +++ b/test/jasmine/tests/cartesian_interact_test.js @@ -1535,6 +1535,59 @@ describe('axis zoom/pan and main plot zoom', function() { .catch(failTest) .then(done); }); + + it('panning a matching axes with references to *missing* axes', function(done) { + var data = [ + // N.B. no traces on subplot xy + { x: [1, 2, 3], y: [1, 2, 1], xaxis: 'x2', yaxis: 'y2'}, + { x: [1, 2, 3], y: [1, 2, 1], xaxis: 'x3', yaxis: 'y3'}, + { x: [1, 2, 3], y: [1, 2, 1], xaxis: 'x4', yaxis: 'y4'} + ]; + + var layout = { + xaxis: {domain: [0, 0.48]}, + xaxis2: {anchor: 'y2', domain: [0.52, 1], matches: 'x'}, + xaxis3: {anchor: 'y3', domain: [0, 0.48], matches: 'x'}, + xaxis4: {anchor: 'y4', domain: [0.52, 1], matches: 'x'}, + yaxis: {domain: [0, 0.48]}, + yaxis2: {anchor: 'x2', domain: [0.52, 1], matches: 'y'}, + yaxis3: {anchor: 'x3', domain: [0.52, 1], matches: 'y'}, + yaxis4: {anchor: 'x4', domain: [0, 0.48], matches: 'y'}, + width: 400, + height: 400, + margin: {t: 50, l: 50, b: 50, r: 50}, + showlegend: false, + dragmode: 'pan' + }; + + makePlot(data, layout).then(function() { + assertRanges('base', [ + [['xaxis', 'xaxis2', 'xaxis3', 'xaxis4'], [0.8206, 3.179]], + [['yaxis', 'yaxis2', 'yaxis3', 'yaxis4'], [0.9103, 2.0896]] + ]); + }) + .then(function() { + var drag = makeDragFns('x2y2', 'nsew', 30, 30); + return drag.start().then(function() { + assertRanges('during drag', [ + [['xaxis', 'xaxis2', 'xaxis3', 'xaxis4'], [0.329, 2.687], {skipInput: true}], + [['yaxis', 'yaxis2', 'yaxis3', 'yaxis4'], [1.156, 2.335], {skipInput: true}] + ]); + }) + .then(drag.end); + }) + .then(_assert('after drag on x2y2 subplot', [ + [['xaxis', 'xaxis2', 'xaxis3', 'xaxis4'], [0.329, 2.687], {dragged: true}], + [['yaxis', 'yaxis2', 'yaxis3', 'yaxis4'], [1.156, 2.335], {dragged: true}] + ])) + .then(doDblClick('x3y3', 'nsew')) + .then(_assert('after double-click on x3y3 subplot', [ + [['xaxis', 'xaxis2', 'xaxis3', 'xaxis4'], [0.8206, 3.179], {autorange: true}], + [['yaxis', 'yaxis2', 'yaxis3', 'yaxis4'], [0.9103, 2.0896], {autorange: true}] + ])) + .catch(failTest) + .then(done); + }); }); describe('redrag behavior', function() {