Skip to content

Commit 6f2bb78

Browse files
committed
first cut at static ax.matches behavior
1 parent 6813865 commit 6f2bb78

File tree

7 files changed

+246
-22
lines changed

7 files changed

+246
-22
lines changed

src/plot_api/plot_api.js

+24-5
Original file line numberDiff line numberDiff line change
@@ -1948,6 +1948,10 @@ function axRangeSupplyDefaultsByPass(gd, flags, specs) {
19481948
axOut.range = axIn.range.slice();
19491949
axOut.cleanRange();
19501950
}
1951+
1952+
// no need to consider matching axes here,
1953+
// if we keep block in doAutoRangeAndConstraints
1954+
19511955
return true;
19521956
}
19531957

@@ -1957,14 +1961,29 @@ function addAxRangeSequence(seq, rangesAltered) {
19571961
// executed after drawData
19581962
var drawAxes = rangesAltered ?
19591963
function(gd) {
1960-
var opts = {skipTitle: true};
1964+
var axIds = [];
1965+
var skipTitle = true;
1966+
var matchGroups = gd._fullLayout._axisMatchGroups || [];
1967+
19611968
for(var id in rangesAltered) {
1962-
if(Axes.getFromId(gd, id).automargin) {
1963-
opts = {};
1964-
break;
1969+
var ax = Axes.getFromId(gd, id);
1970+
axIds.push(id);
1971+
1972+
for(var i = 0; i < matchGroups.length; i++) {
1973+
var group = matchGroups[i];
1974+
if(group[id]) {
1975+
for(var id2 in group) {
1976+
if(!rangesAltered[id2]) {
1977+
axIds.push(id2);
1978+
}
1979+
}
1980+
}
19651981
}
1982+
1983+
if(ax.automargin) skipTitle = false;
19661984
}
1967-
return Axes.draw(gd, Object.keys(rangesAltered), opts);
1985+
1986+
return Axes.draw(gd, axIds, {skipTitle: skipTitle});
19681987
} :
19691988
function(gd) {
19701989
return Axes.draw(gd, 'redraw');

src/plot_api/subroutines.js

+35-3
Original file line numberDiff line numberDiff line change
@@ -697,16 +697,48 @@ exports.redrawReglTraces = function(gd) {
697697
};
698698

699699
exports.doAutoRangeAndConstraints = function(gd) {
700+
var fullLayout = gd._fullLayout;
700701
var axList = Axes.list(gd, '', true);
702+
var matchGroups = fullLayout._axisMatchGroups || [];
703+
var ax;
701704

702705
for(var i = 0; i < axList.length; i++) {
703-
var ax = axList[i];
706+
ax = axList[i];
704707
cleanAxisConstraints(gd, ax);
705-
// in case margins changed, update scale
706-
ax.setScale();
707708
doAutoRange(gd, ax);
708709
}
709710

711+
// TODO bypass this when matching axes aren't autoranged?
712+
for(var j = 0; j < matchGroups.length; j++) {
713+
var group = matchGroups[j];
714+
var rng = null;
715+
var id;
716+
717+
for(id in group) {
718+
ax = Axes.getFromId(gd, id);
719+
720+
if(rng) {
721+
if(rng[0] < rng[1]) {
722+
rng[0] = Math.min(rng[0], ax.range[0]);
723+
rng[1] = Math.max(rng[1], ax.range[1]);
724+
} else {
725+
rng[0] = Math.max(rng[0], ax.range[0]);
726+
rng[1] = Math.min(rng[1], ax.range[1]);
727+
}
728+
} else {
729+
rng = ax.range;
730+
}
731+
}
732+
733+
for(id in group) {
734+
ax = Axes.getFromId(gd, id);
735+
ax.range = rng.slice();
736+
ax._input.range = rng.slice();
737+
ax.setScale(0);
738+
}
739+
}
740+
741+
// TODO before or after matching axes?
710742
enforceAxisConstraints(gd);
711743
};
712744

src/plots/cartesian/autorange.js

+2
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,8 @@ function concatExtremes(gd, ax) {
237237
}
238238

239239
function doAutoRange(gd, ax) {
240+
ax.setScale();
241+
240242
if(ax.autorange) {
241243
ax.range = getAutoRange(gd, ax);
242244

src/plots/cartesian/constraints.js

+25-6
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ var FROM_BL = require('../../constants/alignment').FROM_BL;
1919

2020
exports.handleConstraintDefaults = function(containerIn, containerOut, coerce, allAxisIds, layoutOut) {
2121
var constraintGroups = layoutOut._axisConstraintGroups;
22+
var matchGroups = layoutOut._axisMatchGroups;
2223
var thisID = containerOut._id;
2324
var letter = thisID.charAt(0);
2425

@@ -35,30 +36,48 @@ exports.handleConstraintDefaults = function(containerIn, containerOut, coerce, a
3536
}
3637
}, 'constraintoward');
3738

38-
if(!containerIn.scaleanchor) return;
39+
if(!containerIn.scaleanchor && !containerIn.matches) return;
3940

40-
var constraintOpts = getConstraintOpts(constraintGroups, thisID, allAxisIds, layoutOut);
41+
var opts = getConstraintOpts(constraintGroups, thisID, allAxisIds, layoutOut);
4142

4243
var scaleanchor = Lib.coerce(containerIn, containerOut, {
4344
scaleanchor: {
4445
valType: 'enumerated',
45-
values: constraintOpts.linkableAxes
46+
values: opts.linkableAxes
4647
}
4748
}, 'scaleanchor');
4849

50+
var matches = Lib.coerce(containerIn, containerOut, {
51+
matches: {
52+
valType: 'enumerated',
53+
values: opts.linkableAxes
54+
}
55+
}, 'matches');
56+
57+
var found = false;
58+
4959
if(scaleanchor) {
5060
var scaleratio = coerce('scaleratio');
61+
5162
// TODO: I suppose I could do attribute.min: Number.MIN_VALUE to avoid zero,
5263
// but that seems hacky. Better way to say "must be a positive number"?
5364
// Of course if you use several super-tiny values you could eventually
5465
// force a product of these to zero and all hell would break loose...
5566
// Likewise with super-huge values.
5667
if(!scaleratio) scaleratio = containerOut.scaleratio = 1;
5768

58-
updateConstraintGroups(constraintGroups, constraintOpts.thisGroup,
59-
thisID, scaleanchor, scaleratio);
69+
updateConstraintGroups(constraintGroups, opts.thisGroup, thisID, scaleanchor, scaleratio);
70+
found = true;
6071
}
61-
else if(allAxisIds.indexOf(containerIn.scaleanchor) !== -1) {
72+
73+
// TODO what happens when both scaleanchor and matches are present???
74+
75+
if(matches) {
76+
updateConstraintGroups(matchGroups, opts.thisGroup, thisID, matches, 1);
77+
found = true;
78+
}
79+
80+
if(!found && allAxisIds.indexOf(containerIn.scaleanchor) !== -1) {
6281
Lib.warn('ignored ' + containerOut._name + '.scaleanchor: "' +
6382
containerIn.scaleanchor + '" to avoid either an infinite loop ' +
6483
'and possibly inconsistent scaleratios, or because the target' +

src/plots/cartesian/layout_attributes.js

+17
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,23 @@ module.exports = {
211211
'and *right* for x axes, and *top*, *middle* (default), and *bottom* for y axes.'
212212
].join(' ')
213213
},
214+
matches: {
215+
valType: 'enumerated',
216+
values: [
217+
constants.idRegex.x.toString(),
218+
constants.idRegex.y.toString()
219+
],
220+
role: 'info',
221+
editType: 'calc',
222+
description: [
223+
'If set to another axis id (e.g. `x2`, `y`), the range of this axis',
224+
'will match the range of the corresponding axis in data-coordinates space.',
225+
'Moreover, matching axes share auto-range values, category lists and',
226+
'histogram auto-bins.',
227+
'Note that setting `matches` and `scaleratio` under a *range* `constrain`',
228+
'to the same axis id is forbidden.'
229+
].join(' ')
230+
},
214231
// ticks
215232
tickmode: {
216233
valType: 'enumerated',

src/plots/cartesian/layout_defaults.js

+41-8
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
124124
}
125125

126126
var counterAxes = {x: getCounterAxes('x'), y: getCounterAxes('y')};
127+
var allAxisIds = counterAxes.x.concat(counterAxes.y);
127128

128129
function getOverlayableAxes(axLetter, axName) {
129130
var list = (axLetter === 'x') ? xNames : yNames;
@@ -199,14 +200,12 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
199200
delete axLayoutOut.spikesnap;
200201
}
201202

202-
var positioningOptions = {
203+
handlePositionDefaults(axLayoutIn, axLayoutOut, coerce, {
203204
letter: axLetter,
204205
counterAxes: counterAxes[axLetter],
205206
overlayableAxes: overlayableAxes,
206207
grid: layoutOut.grid
207-
};
208-
209-
handlePositionDefaults(axLayoutIn, axLayoutOut, coerce, positioningOptions);
208+
});
210209

211210
axLayoutOut._input = axLayoutIn;
212211
}
@@ -247,22 +246,56 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
247246
coerce('fixedrange', fixedRangeDflt);
248247
}
249248

250-
// Finally, handle scale constraints. We need to do this after all axes have
251-
// coerced both `type` (so we link only axes of the same type) and
249+
// Finally, handle scale constraints and matching axes.
250+
//
251+
// We need to do this after all axes have coerced both `type`
252+
// (so we link only axes of the same type) and
252253
// `fixedrange` (so we can avoid linking from OR TO a fixed axis).
253254

254255
// sets of axes linked by `scaleanchor` along with the scaleratios compounded
255256
// together, populated in handleConstraintDefaults
256257
layoutOut._axisConstraintGroups = [];
257-
var allAxisIds = counterAxes.x.concat(counterAxes.y);
258+
// similar to _axisConstraintGroups, but for matching axes
259+
layoutOut._axisMatchGroups = [];
258260

259261
for(i = 0; i < axNames.length; i++) {
260262
axName = axNames[i];
261263
axLetter = axName.charAt(0);
262-
263264
axLayoutIn = layoutIn[axName];
264265
axLayoutOut = layoutOut[axName];
265266

266267
handleConstraintDefaults(axLayoutIn, axLayoutOut, coerce, allAxisIds, layoutOut);
267268
}
269+
270+
for(i = 0; i < layoutOut._axisMatchGroups.length; i++) {
271+
var group = layoutOut._axisMatchGroups[i];
272+
var rng = null;
273+
var autorange = null;
274+
var axId;
275+
276+
for(axId in group) {
277+
axLayoutOut = layoutOut[id2name(axId)];
278+
if(!axLayoutOut.matches) {
279+
rng = axLayoutOut.range;
280+
autorange = axLayoutOut.autorange;
281+
}
282+
}
283+
284+
if(rng === null || autorange === null) {
285+
for(axId in group) {
286+
axLayoutOut = layoutOut[id2name(axId)];
287+
rng = axLayoutOut.range;
288+
autorange = axLayoutOut.autorange;
289+
break;
290+
}
291+
}
292+
293+
for(axId in group) {
294+
axLayoutOut = layoutOut[id2name(axId)];
295+
if(axLayoutOut.matches) {
296+
axLayoutOut.range = rng.slice();
297+
axLayoutOut.autorange = autorange;
298+
}
299+
}
300+
}
268301
};

0 commit comments

Comments
 (0)