Skip to content

Handle 'missing' matching axes #4529

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jan 31, 2020
Prev Previous commit
Next Next commit
track & coerce 'missing' matching axes
- keep list of valid but missing `matches` value
- coerce the missing axes in fullLayout
- include the missing axes in the list of valid `matches` values
- add a few comments about the variables used in the cartesian
  supplyLayoutDefaults routine.
- add jasmine supplyDefaults tests!
  • Loading branch information
etpinard committed Jan 29, 2020
commit 33078b0c12e17b5cee7e7240f430da67bac1a44c
124 changes: 99 additions & 25 deletions src/plots/cartesian/layout_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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 = [];
Expand All @@ -165,9 +185,26 @@ 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);
// list of axis ids that axes in axNames have a reference to,
// even though they are missing from allAxisIds
var missingMatchedAxisIds = [];

// fill in 'missing' axis list when an axis is set to match an axis
// not part of the allAxisIds list
function addMissingMatchedAxis(matchesIn) {
if(AX_ID_PATTERN.test(matchesIn) && allAxisIds.indexOf(matchesIn) === -1) {
Lib.pushUnique(missingMatchedAxisIds, matchesIn);
}
}

// 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])) {
Expand All @@ -176,20 +213,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]) ||
Expand All @@ -207,13 +231,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);
Expand All @@ -239,12 +263,60 @@ 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(axLayoutIn.matches);

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);

handleTypeDefaults(axLayoutIn, axLayoutOut, coerce, defaultOptions2);
handleAxisDefaults(axLayoutIn, axLayoutOut, coerce, defaultOptions2, layoutOut);

handlePositionDefaults(axLayoutIn, axLayoutOut, coerce, {
letter: axLetter,
counterAxes: counterAxes[axLetter],
overlayableAxes: getOverlayableAxes(axLetter, axName),
grid: layoutOut.grid
});

addMissingMatchedAxis(axLayoutIn.matches);

axLayoutOut._input = axLayoutIn;
}

Expand Down Expand Up @@ -295,9 +367,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];
Expand All @@ -317,7 +392,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
}

handleConstraintDefaults(axLayoutIn, axLayoutOut, coerce, {
allAxisIds: allAxisIds,
allAxisIds: allAxisIdsIncludingMissing,
layoutOut: layoutOut,
scaleanchorDflt: scaleanchorDflt,
constrainDflt: constrainDflt
Expand All @@ -328,7 +403,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) {
Expand Down
107 changes: 107 additions & 0 deletions test/jasmine/tests/axes_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.push('x2y2', 'x3y3', 'x4y4');
layoutOut._subplots.xaxis.push('x2', 'x3', 'x4');
layoutOut._subplots.yaxis.push('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.push('x3y3', 'x4y4');
layoutOut._subplots.xaxis.push('x3', 'x4');
layoutOut._subplots.yaxis.push('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
Expand Down Expand Up @@ -871,6 +933,51 @@ 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.push('x2y2', 'x4y4');
layoutOut._subplots.xaxis.push('x2', 'x4');
layoutOut._subplots.yaxis.push('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.push('x3y3', 'x4y4');
layoutOut._subplots.xaxis.push('x3', 'x4');
layoutOut._subplots.yaxis.push('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 adapt default axis ranges to *rangemode*', function() {
layoutIn = {
xaxis: {rangemode: 'tozero'},
Expand Down