Skip to content

Commit 51a479b

Browse files
authored
Merge pull request #1767 from plotly/constrain-domain
Constrain axes by domain
2 parents 2b831a0 + 67ea3a8 commit 51a479b

24 files changed

+818
-223
lines changed

src/constants/alignment.js

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Copyright 2012-2017, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
// fraction of some size to get to a named position
12+
module.exports = {
13+
// from bottom left: this is the origin of our paper-reference
14+
// positioning system
15+
FROM_BL: {
16+
left: 0,
17+
center: 0.5,
18+
right: 1,
19+
bottom: 0,
20+
middle: 0.5,
21+
top: 1
22+
},
23+
// from top left: this is the screen pixel positioning origin
24+
FROM_TL: {
25+
left: 0,
26+
center: 0.5,
27+
right: 1,
28+
bottom: 1,
29+
middle: 0.5,
30+
top: 0
31+
}
32+
};

src/lib/coerce.js

+14
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,20 @@ exports.valObjects = {
4444
if(opts.coerceNumber) v = +v;
4545
if(opts.values.indexOf(v) === -1) propOut.set(dflt);
4646
else propOut.set(v);
47+
},
48+
validateFunction: function(v, opts) {
49+
if(opts.coerceNumber) v = +v;
50+
51+
var values = opts.values;
52+
for(var i = 0; i < values.length; i++) {
53+
var k = String(values[i]);
54+
55+
if((k.charAt(0) === '/' && k.charAt(k.length - 1) === '/')) {
56+
var regex = new RegExp(k.substr(1, k.length - 2));
57+
if(regex.test(v)) return true;
58+
} else if(v === values[i]) return true;
59+
}
60+
return false;
4761
}
4862
},
4963
'boolean': {

src/plot_api/plot_api.js

+37-14
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ var manageArrays = require('./manage_arrays');
3232
var helpers = require('./helpers');
3333
var subroutines = require('./subroutines');
3434
var cartesianConstants = require('../plots/cartesian/constants');
35-
var enforceAxisConstraints = require('../plots/cartesian/constraints');
35+
var axisConstraints = require('../plots/cartesian/constraints');
36+
var enforceAxisConstraints = axisConstraints.enforce;
37+
var cleanAxisConstraints = axisConstraints.clean;
3638
var axisIds = require('../plots/cartesian/axis_ids');
3739

3840

@@ -190,8 +192,7 @@ Plotly.plot = function(gd, data, layout, config) {
190192

191193
return Lib.syncOrAsync([
192194
subroutines.layoutStyles,
193-
drawAxes,
194-
initInteractions
195+
drawAxes
195196
], gd);
196197
}
197198

@@ -220,19 +221,19 @@ Plotly.plot = function(gd, data, layout, config) {
220221

221222
// in case the margins changed, draw margin pushers again
222223
function marginPushersAgain() {
223-
var seq = JSON.stringify(fullLayout._size) === oldmargins ?
224-
[] :
225-
[marginPushers, subroutines.layoutStyles];
224+
if(JSON.stringify(fullLayout._size) === oldmargins) return;
226225

227-
// re-initialize cartesian interaction,
228-
// which are sometimes cleared during marginPushers
229-
seq = seq.concat(initInteractions);
230-
231-
return Lib.syncOrAsync(seq, gd);
226+
return Lib.syncOrAsync([
227+
marginPushers,
228+
subroutines.layoutStyles
229+
], gd);
232230
}
233231

234232
function positionAndAutorange() {
235-
if(!recalc) return;
233+
if(!recalc) {
234+
enforceAxisConstraints(gd);
235+
return;
236+
}
236237

237238
var subplots = Plots.getSubplotIds(fullLayout, 'cartesian'),
238239
modules = fullLayout._modules;
@@ -270,7 +271,10 @@ Plotly.plot = function(gd, data, layout, config) {
270271

271272
var axList = Plotly.Axes.list(gd, '', true);
272273
for(var i = 0; i < axList.length; i++) {
273-
Plotly.Axes.doAutoRange(axList[i]);
274+
var ax = axList[i];
275+
cleanAxisConstraints(gd, ax);
276+
277+
Plotly.Axes.doAutoRange(ax);
274278
}
275279

276280
enforceAxisConstraints(gd);
@@ -365,11 +369,13 @@ Plotly.plot = function(gd, data, layout, config) {
365369
drawFramework,
366370
marginPushers,
367371
marginPushersAgain,
372+
initInteractions,
368373
positionAndAutorange,
369374
subroutines.layoutStyles,
370375
drawAxes,
371376
drawData,
372377
finalDraw,
378+
initInteractions,
373379
Plots.rehover
374380
];
375381

@@ -1917,10 +1923,12 @@ function _relayout(gd, aobj) {
19171923
// we're editing the (auto)range of, so we can tell the others constrained
19181924
// to scale with them that it's OK for them to shrink
19191925
var rangesAltered = {};
1926+
var axId;
19201927

19211928
function recordAlteredAxis(pleafPlus) {
19221929
var axId = axisIds.name2id(pleafPlus.split('.')[0]);
19231930
rangesAltered[axId] = 1;
1931+
return axId;
19241932
}
19251933

19261934
// alter gd.layout
@@ -1963,11 +1971,25 @@ function _relayout(gd, aobj) {
19631971
else if(pleafPlus.match(/^[xyz]axis[0-9]*\.range(\[[0|1]\])?$/)) {
19641972
doextra(ptrunk + '.autorange', false);
19651973
recordAlteredAxis(pleafPlus);
1974+
Lib.nestedProperty(fullLayout, ptrunk + '._inputRange').set(null);
19661975
}
19671976
else if(pleafPlus.match(/^[xyz]axis[0-9]*\.autorange$/)) {
19681977
doextra([ptrunk + '.range[0]', ptrunk + '.range[1]'],
19691978
undefined);
19701979
recordAlteredAxis(pleafPlus);
1980+
Lib.nestedProperty(fullLayout, ptrunk + '._inputRange').set(null);
1981+
var axFull = Lib.nestedProperty(fullLayout, ptrunk).get();
1982+
if(axFull._inputDomain) {
1983+
// if we're autoranging and this axis has a constrained domain,
1984+
// reset it so we don't get locked into a shrunken size
1985+
axFull._input.domain = axFull._inputDomain.slice();
1986+
}
1987+
}
1988+
else if(pleafPlus.match(/^[xyz]axis[0-9]*\.domain(\[[0|1]\])?$/)) {
1989+
Lib.nestedProperty(fullLayout, ptrunk + '._inputDomain').set(null);
1990+
}
1991+
else if(pleafPlus.match(/^[xyz]axis[0-9]*\.constrain.*$/)) {
1992+
flags.docalc = true;
19711993
}
19721994
else if(pleafPlus.match(/^aspectratio\.[xyz]$/)) {
19731995
doextra(proot + '.aspectmode', 'manual');
@@ -2047,6 +2069,7 @@ function _relayout(gd, aobj) {
20472069
// will not make sense, so autorange it.
20482070
doextra(ptrunk + '.autorange', true);
20492071
}
2072+
Lib.nestedProperty(fullLayout, ptrunk + '._inputRange').set(null);
20502073
}
20512074
else if(pleaf.match(cartesianConstants.AX_NAME_PATTERN)) {
20522075
var fullProp = Lib.nestedProperty(fullLayout, ai).get(),
@@ -2193,7 +2216,7 @@ function _relayout(gd, aobj) {
21932216

21942217
// figure out if we need to recalculate axis constraints
21952218
var constraints = fullLayout._axisConstraintGroups;
2196-
for(var axId in rangesAltered) {
2219+
for(axId in rangesAltered) {
21972220
for(i = 0; i < constraints.length; i++) {
21982221
var group = constraints[i];
21992222
if(group[axId]) {

src/plot_api/validate.js

+18-3
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,11 @@ function crawl(objIn, objOut, schema, list, base, path) {
219219
else if(!Lib.validate(valIn, nestedSchema)) {
220220
list.push(format('value', base, p, valIn));
221221
}
222+
else if(nestedSchema.valType === 'enumerated' &&
223+
((nestedSchema.coerceNumber && valIn !== +valOut) || valIn !== valOut)
224+
) {
225+
list.push(format('dynamic', base, p, valIn, valOut));
226+
}
222227
}
223228

224229
return list;
@@ -267,6 +272,16 @@ var code2msgFunc = {
267272

268273
return inBase(base) + target + ' ' + astr + ' did not get coerced';
269274
},
275+
dynamic: function(base, astr, valIn, valOut) {
276+
return [
277+
inBase(base) + 'key',
278+
astr,
279+
'(set to \'' + valIn + '\')',
280+
'got reset to',
281+
'\'' + valOut + '\'',
282+
'during defaults.'
283+
].join(' ');
284+
},
270285
invisible: function(base) {
271286
return 'Trace ' + base[1] + ' got defaulted to be not visible';
272287
},
@@ -284,7 +299,7 @@ function inBase(base) {
284299
return 'In ' + base + ', ';
285300
}
286301

287-
function format(code, base, path, valIn) {
302+
function format(code, base, path, valIn, valOut) {
288303
path = path || '';
289304

290305
var container, trace;
@@ -301,8 +316,8 @@ function format(code, base, path, valIn) {
301316
trace = null;
302317
}
303318

304-
var astr = convertPathToAttributeString(path),
305-
msg = code2msgFunc[code](base, astr, valIn);
319+
var astr = convertPathToAttributeString(path);
320+
var msg = code2msgFunc[code](base, astr, valIn, valOut);
306321

307322
// log to console if logger config option is enabled
308323
Lib.log(msg);

src/plots/cartesian/axes.js

+7
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,13 @@ axes.expand = function(ax, data, options) {
455455
i, j, v, di, dmin, dmax,
456456
ppadiplus, ppadiminus, includeThis, vmin, vmax;
457457

458+
// domain-constrained axes: base extrappad on the unconstrained
459+
// domain so it's consistent as the domain changes
460+
if(extrappad && (ax.constrain === 'domain') && ax._inputDomain) {
461+
extrappad *= (ax._inputDomain[1] - ax._inputDomain[0]) /
462+
(ax.domain[1] - ax.domain[0]);
463+
}
464+
458465
function getPad(item) {
459466
if(Array.isArray(item)) {
460467
return function(i) { return Math.max(Number(item[i]||0), 0); };

src/plots/cartesian/constraint_defaults.js

+18-3
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,25 @@ var id2name = require('./axis_ids').id2name;
1515

1616
module.exports = function handleConstraintDefaults(containerIn, containerOut, coerce, allAxisIds, layoutOut) {
1717
var constraintGroups = layoutOut._axisConstraintGroups;
18+
var thisID = containerOut._id;
19+
var letter = thisID.charAt(0);
1820

19-
if(containerOut.fixedrange || !containerIn.scaleanchor) return;
21+
if(containerOut.fixedrange) return;
2022

21-
var constraintOpts = getConstraintOpts(constraintGroups, containerOut._id, allAxisIds, layoutOut);
23+
// coerce the constraint mechanics even if this axis has no scaleanchor
24+
// because it may be the anchor of another axis.
25+
coerce('constrain');
26+
Lib.coerce(containerIn, containerOut, {
27+
constraintoward: {
28+
valType: 'enumerated',
29+
values: letter === 'x' ? ['left', 'center', 'right'] : ['bottom', 'middle', 'top'],
30+
dflt: letter === 'x' ? 'center' : 'middle'
31+
}
32+
}, 'constraintoward');
33+
34+
if(!containerIn.scaleanchor) return;
35+
36+
var constraintOpts = getConstraintOpts(constraintGroups, thisID, allAxisIds, layoutOut);
2237

2338
var scaleanchor = Lib.coerce(containerIn, containerOut, {
2439
scaleanchor: {
@@ -37,7 +52,7 @@ module.exports = function handleConstraintDefaults(containerIn, containerOut, co
3752
if(!scaleratio) scaleratio = containerOut.scaleratio = 1;
3853

3954
updateConstraintGroups(constraintGroups, constraintOpts.thisGroup,
40-
containerOut._id, scaleanchor, scaleratio);
55+
thisID, scaleanchor, scaleratio);
4156
}
4257
else if(allAxisIds.indexOf(containerIn.scaleanchor) !== -1) {
4358
Lib.warn('ignored ' + containerOut._name + '.scaleanchor: "' +

0 commit comments

Comments
 (0)