Skip to content

Commit 33078b0

Browse files
committed
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!
1 parent 0984974 commit 33078b0

File tree

2 files changed

+206
-25
lines changed

2 files changed

+206
-25
lines changed

src/plots/cartesian/layout_defaults.js

+99-25
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ var axisIds = require('./axis_ids');
2424
var id2name = axisIds.id2name;
2525
var name2id = axisIds.name2id;
2626

27+
var AX_ID_PATTERN = require('./constants').AX_ID_PATTERN;
28+
2729
var Registry = require('../../registry');
2830
var traceIs = Registry.traceIs;
2931
var getComponentMethod = Registry.getComponentMethod;
@@ -133,7 +135,28 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
133135

134136
var bgColor = Color.combine(plotBgColor, layoutOut.paper_bgcolor);
135137

136-
var axName, axLetter, axLayoutIn, axLayoutOut;
138+
// name of single axis (e.g. 'xaxis', 'yaxis2')
139+
var axName;
140+
// id of single axis (e.g. 'y', 'x5')
141+
var axId;
142+
// 'x' or 'y'
143+
var axLetter;
144+
// input layout axis container
145+
var axLayoutIn;
146+
// full layout axis container
147+
var axLayoutOut;
148+
149+
function newAxLayoutOut() {
150+
var traces = ax2traces[axName] || [];
151+
axLayoutOut._traceIndices = traces.map(function(t) { return t._expandedIndex; });
152+
axLayoutOut._annIndices = [];
153+
axLayoutOut._shapeIndices = [];
154+
axLayoutOut._imgIndices = [];
155+
axLayoutOut._subplotsWith = [];
156+
axLayoutOut._counterAxes = [];
157+
axLayoutOut._name = axLayoutOut._attr = axName;
158+
axLayoutOut._id = axId;
159+
}
137160

138161
function coerce(attr, dflt) {
139162
return Lib.coerce(axLayoutIn, axLayoutOut, layoutAttributes, attr, dflt);
@@ -147,9 +170,6 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
147170
return (axLetter === 'x') ? yIds : xIds;
148171
}
149172

150-
var counterAxes = {x: getCounterAxes('x'), y: getCounterAxes('y')};
151-
var allAxisIds = counterAxes.x.concat(counterAxes.y);
152-
153173
function getOverlayableAxes(axLetter, axName) {
154174
var list = (axLetter === 'x') ? xNames : yNames;
155175
var out = [];
@@ -165,9 +185,26 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
165185
return out;
166186
}
167187

188+
// list of available counter axis names
189+
var counterAxes = {x: getCounterAxes('x'), y: getCounterAxes('y')};
190+
// list of all x AND y axis ids
191+
var allAxisIds = counterAxes.x.concat(counterAxes.y);
192+
// list of axis ids that axes in axNames have a reference to,
193+
// even though they are missing from allAxisIds
194+
var missingMatchedAxisIds = [];
195+
196+
// fill in 'missing' axis list when an axis is set to match an axis
197+
// not part of the allAxisIds list
198+
function addMissingMatchedAxis(matchesIn) {
199+
if(AX_ID_PATTERN.test(matchesIn) && allAxisIds.indexOf(matchesIn) === -1) {
200+
Lib.pushUnique(missingMatchedAxisIds, matchesIn);
201+
}
202+
}
203+
168204
// first pass creates the containers, determines types, and handles most of the settings
169205
for(i = 0; i < axNames.length; i++) {
170206
axName = axNames[i];
207+
axId = name2id(axName);
171208
axLetter = axName.charAt(0);
172209

173210
if(!Lib.isPlainObject(layoutIn[axName])) {
@@ -176,20 +213,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
176213

177214
axLayoutIn = layoutIn[axName];
178215
axLayoutOut = Template.newContainer(layoutOut, axName, axLetter + 'axis');
179-
180-
var traces = ax2traces[axName] || [];
181-
axLayoutOut._traceIndices = traces.map(function(t) { return t._expandedIndex; });
182-
axLayoutOut._annIndices = [];
183-
axLayoutOut._shapeIndices = [];
184-
axLayoutOut._imgIndices = [];
185-
axLayoutOut._subplotsWith = [];
186-
axLayoutOut._counterAxes = [];
187-
188-
// set up some private properties
189-
axLayoutOut._name = axLayoutOut._attr = axName;
190-
var id = axLayoutOut._id = name2id(axName);
191-
192-
var overlayableAxes = getOverlayableAxes(axLetter, axName);
216+
newAxLayoutOut();
193217

194218
var visibleDflt =
195219
(axLetter === 'x' && !xaMustDisplay[axName] && xaMayHide[axName]) ||
@@ -207,13 +231,13 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
207231
font: layoutOut.font,
208232
outerTicks: outerTicks[axName],
209233
showGrid: !noGrids[axName],
210-
data: traces,
234+
data: ax2traces[axName] || [],
211235
bgColor: bgColor,
212236
calendar: layoutOut.calendar,
213237
automargin: true,
214238
visibleDflt: visibleDflt,
215239
reverseDflt: reverseDflt,
216-
splomStash: ((layoutOut._splomAxes || {})[axLetter] || {})[id]
240+
splomStash: ((layoutOut._splomAxes || {})[axLetter] || {})[axId]
217241
};
218242

219243
coerce('uirevision', layoutOut.uirevision);
@@ -239,12 +263,60 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
239263
handlePositionDefaults(axLayoutIn, axLayoutOut, coerce, {
240264
letter: axLetter,
241265
counterAxes: counterAxes[axLetter],
242-
overlayableAxes: overlayableAxes,
266+
overlayableAxes: getOverlayableAxes(axLetter, axName),
243267
grid: layoutOut.grid
244268
});
245269

246270
coerce('title.standoff');
247271

272+
addMissingMatchedAxis(axLayoutIn.matches);
273+
274+
axLayoutOut._input = axLayoutIn;
275+
}
276+
277+
// coerce the 'missing' axes
278+
i = 0;
279+
while(i < missingMatchedAxisIds.length) {
280+
axId = missingMatchedAxisIds[i++];
281+
axName = id2name(axId);
282+
axLetter = axName.charAt(0);
283+
284+
if(!Lib.isPlainObject(layoutIn[axName])) {
285+
layoutIn[axName] = {};
286+
}
287+
288+
axLayoutIn = layoutIn[axName];
289+
axLayoutOut = Template.newContainer(layoutOut, axName, axLetter + 'axis');
290+
newAxLayoutOut();
291+
292+
var defaultOptions2 = {
293+
letter: axLetter,
294+
font: layoutOut.font,
295+
outerTicks: outerTicks[axName],
296+
showGrid: !noGrids[axName],
297+
data: [],
298+
bgColor: bgColor,
299+
calendar: layoutOut.calendar,
300+
automargin: true,
301+
visibleDflt: false,
302+
reverseDflt: false,
303+
splomStash: ((layoutOut._splomAxes || {})[axLetter] || {})[axId]
304+
};
305+
306+
coerce('uirevision', layoutOut.uirevision);
307+
308+
handleTypeDefaults(axLayoutIn, axLayoutOut, coerce, defaultOptions2);
309+
handleAxisDefaults(axLayoutIn, axLayoutOut, coerce, defaultOptions2, layoutOut);
310+
311+
handlePositionDefaults(axLayoutIn, axLayoutOut, coerce, {
312+
letter: axLetter,
313+
counterAxes: counterAxes[axLetter],
314+
overlayableAxes: getOverlayableAxes(axLetter, axName),
315+
grid: layoutOut.grid
316+
});
317+
318+
addMissingMatchedAxis(axLayoutIn.matches);
319+
248320
axLayoutOut._input = axLayoutIn;
249321
}
250322

@@ -295,9 +367,12 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
295367
var constraintGroups = layoutOut._axisConstraintGroups = [];
296368
// similar to _axisConstraintGroups, but for matching axes
297369
var matchGroups = layoutOut._axisMatchGroups = [];
370+
// make sure to include 'missing' axes here
371+
var allAxisIdsIncludingMissing = allAxisIds.concat(missingMatchedAxisIds);
372+
var axNamesIncludingMissing = axNames.concat(Lib.simpleMap(missingMatchedAxisIds, id2name));
298373

299-
for(i = 0; i < axNames.length; i++) {
300-
axName = axNames[i];
374+
for(i = 0; i < axNamesIncludingMissing.length; i++) {
375+
axName = axNamesIncludingMissing[i];
301376
axLetter = axName.charAt(0);
302377
axLayoutIn = layoutIn[axName];
303378
axLayoutOut = layoutOut[axName];
@@ -317,7 +392,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
317392
}
318393

319394
handleConstraintDefaults(axLayoutIn, axLayoutOut, coerce, {
320-
allAxisIds: allAxisIds,
395+
allAxisIds: allAxisIdsIncludingMissing,
321396
layoutOut: layoutOut,
322397
scaleanchorDflt: scaleanchorDflt,
323398
constrainDflt: constrainDflt
@@ -328,7 +403,6 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
328403
var group = matchGroups[i];
329404
var rng = null;
330405
var autorange = null;
331-
var axId;
332406

333407
// find 'matching' range attrs
334408
for(axId in group) {

test/jasmine/tests/axes_test.js

+107
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,68 @@ describe('Test axes', function() {
830830
expect(layoutOut._axisMatchGroups).toContain({y: 1, y2: 1, y3: 1});
831831
});
832832

833+
it('should find matching group even when matching a *missing* axis', function() {
834+
layoutIn = {
835+
// N.B. xaxis isn't set
836+
xaxis2: {matches: 'x'},
837+
xaxis3: {matches: 'x'},
838+
xaxis4: {matches: 'x'},
839+
// N.B. yaxis isn't set
840+
yaxis2: {matches: 'y'},
841+
yaxis3: {matches: 'y2'},
842+
yaxis4: {matches: 'y3'},
843+
};
844+
layoutOut._subplots.cartesian.push('x2y2', 'x3y3', 'x4y4');
845+
layoutOut._subplots.xaxis.push('x2', 'x3', 'x4');
846+
layoutOut._subplots.yaxis.push('y2', 'y3', 'y4');
847+
848+
supplyLayoutDefaults(layoutIn, layoutOut, fullData);
849+
850+
expect(layoutOut._axisMatchGroups.length).toBe(2);
851+
expect(layoutOut._axisMatchGroups).toContain({x: 1, x2: 1, x3: 1, x4: 1});
852+
expect(layoutOut._axisMatchGroups).toContain({y: 1, y2: 1, y3: 1, y4: 1});
853+
854+
// should coerce the 'missing' axes
855+
expect(layoutIn.xaxis).toBeDefined();
856+
expect(layoutIn.yaxis).toBeDefined();
857+
expect(layoutOut.xaxis).toBeDefined();
858+
expect(layoutOut.yaxis).toBeDefined();
859+
});
860+
861+
it('should find matching group even when matching a *missing* axis (nested case)', function() {
862+
layoutIn = {
863+
// N.B. xaxis isn't set
864+
// N.B. xaxis2 is set, but does not correspond to a subplot
865+
xaxis2: {matches: 'x'},
866+
xaxis3: {matches: 'x2'},
867+
xaxis4: {matches: 'x3'},
868+
// N.B. yaxis isn't set
869+
// N.B yaxis2 does not correspond to a subplot and is useless here
870+
yaxis2: {matches: 'y'},
871+
yaxis3: {matches: 'y'},
872+
yaxis4: {matches: 'y3'}
873+
};
874+
layoutOut._subplots.cartesian.push('x3y3', 'x4y4');
875+
layoutOut._subplots.xaxis.push('x3', 'x4');
876+
layoutOut._subplots.yaxis.push('y3', 'y4');
877+
878+
supplyLayoutDefaults(layoutIn, layoutOut, fullData);
879+
880+
expect(layoutOut._axisMatchGroups.length).toBe(2);
881+
expect(layoutOut._axisMatchGroups).toContain({x: 1, x2: 1, x3: 1, x4: 1});
882+
expect(layoutOut._axisMatchGroups).toContain({y: 1, y3: 1, y4: 1});
883+
884+
// should coerce the 'missing' axes
885+
expect(layoutIn.xaxis).toBeDefined();
886+
expect(layoutIn.yaxis).toBeDefined();
887+
expect(layoutOut.xaxis).toBeDefined();
888+
expect(layoutOut.yaxis).toBeDefined();
889+
890+
// should coerce useless axes
891+
expect(layoutIn.yaxis2).toEqual({matches: 'y'});
892+
expect(layoutOut.yaxis2).toBeUndefined();
893+
});
894+
833895
it('should match set axis range value for matching axes', function() {
834896
layoutIn = {
835897
// autorange case
@@ -871,6 +933,51 @@ describe('Test axes', function() {
871933
_assertMatchingAxes(['xaxis4', 'yaxis4'], false, [-1, 3]);
872934
});
873935

936+
it('should match set axis range value for matching axes even when matching a *missing* axis', function() {
937+
layoutIn = {
938+
// N.B. xaxis is set, but does not correspond to a subplot
939+
xaxis: {range: [0, 1]},
940+
xaxis2: {matches: 'x'},
941+
xaxis4: {matches: 'x'}
942+
};
943+
layoutOut._subplots.cartesian.push('x2y2', 'x4y4');
944+
layoutOut._subplots.xaxis.push('x2', 'x4');
945+
layoutOut._subplots.yaxis.push('y2', 'y4');
946+
947+
supplyLayoutDefaults(layoutIn, layoutOut, fullData);
948+
949+
expect(layoutOut._axisMatchGroups.length).toBe(1);
950+
expect(layoutOut._axisMatchGroups).toContain({x: 1, x2: 1, x4: 1});
951+
952+
expect(layoutOut.xaxis.range).withContext('xaxis.range').toEqual([0, 1]);
953+
expect(layoutOut.xaxis2.range).withContext('xaxis2.range').toEqual([0, 1]);
954+
expect(layoutOut.xaxis4.range).withContext('xaxis4.range').toEqual([0, 1]);
955+
});
956+
957+
it('should match set axis range value for matching axes even when matching a *missing* axis (nested case)', function() {
958+
layoutIn = {
959+
// N.B. xaxis is set, but does not correspond to a subplot
960+
xaxis: {range: [0, 1]},
961+
// N.B. xaxis2 is set, but does not correspond to a subplot
962+
xaxis2: {matches: 'x'},
963+
xaxis3: {matches: 'x2'},
964+
xaxis4: {matches: 'x3'}
965+
};
966+
layoutOut._subplots.cartesian.push('x3y3', 'x4y4');
967+
layoutOut._subplots.xaxis.push('x3', 'x4');
968+
layoutOut._subplots.yaxis.push('y3', 'y4');
969+
970+
supplyLayoutDefaults(layoutIn, layoutOut, fullData);
971+
972+
expect(layoutOut._axisMatchGroups.length).toBe(1);
973+
expect(layoutOut._axisMatchGroups).toContain({x: 1, x2: 1, x3: 1, x4: 1});
974+
975+
expect(layoutOut.xaxis.range).withContext('xaxis.range').toEqual([0, 1]);
976+
expect(layoutOut.xaxis2.range).withContext('xaxis2.range').toEqual([0, 1]);
977+
expect(layoutOut.xaxis2.range).withContext('xaxis3.range').toEqual([0, 1]);
978+
expect(layoutOut.xaxis4.range).withContext('xaxis4.range').toEqual([0, 1]);
979+
});
980+
874981
it('should adapt default axis ranges to *rangemode*', function() {
875982
layoutIn = {
876983
xaxis: {rangemode: 'tozero'},

0 commit comments

Comments
 (0)