Skip to content

Commit

Permalink
Merge pull request #1767 from plotly/constrain-domain
Browse files Browse the repository at this point in the history
Constrain axes by domain
  • Loading branch information
alexcjohnson committed Jun 9, 2017
2 parents 2b831a0 + 67ea3a8 commit 51a479b
Show file tree
Hide file tree
Showing 24 changed files with 818 additions and 223 deletions.
32 changes: 32 additions & 0 deletions src/constants/alignment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Copyright 2012-2017, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

// fraction of some size to get to a named position
module.exports = {
// from bottom left: this is the origin of our paper-reference
// positioning system
FROM_BL: {
left: 0,
center: 0.5,
right: 1,
bottom: 0,
middle: 0.5,
top: 1
},
// from top left: this is the screen pixel positioning origin
FROM_TL: {
left: 0,
center: 0.5,
right: 1,
bottom: 1,
middle: 0.5,
top: 0
}
};
14 changes: 14 additions & 0 deletions src/lib/coerce.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@ exports.valObjects = {
if(opts.coerceNumber) v = +v;
if(opts.values.indexOf(v) === -1) propOut.set(dflt);
else propOut.set(v);
},
validateFunction: function(v, opts) {
if(opts.coerceNumber) v = +v;

var values = opts.values;
for(var i = 0; i < values.length; i++) {
var k = String(values[i]);

if((k.charAt(0) === '/' && k.charAt(k.length - 1) === '/')) {
var regex = new RegExp(k.substr(1, k.length - 2));
if(regex.test(v)) return true;
} else if(v === values[i]) return true;
}
return false;
}
},
'boolean': {
Expand Down
51 changes: 37 additions & 14 deletions src/plot_api/plot_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ var manageArrays = require('./manage_arrays');
var helpers = require('./helpers');
var subroutines = require('./subroutines');
var cartesianConstants = require('../plots/cartesian/constants');
var enforceAxisConstraints = require('../plots/cartesian/constraints');
var axisConstraints = require('../plots/cartesian/constraints');
var enforceAxisConstraints = axisConstraints.enforce;
var cleanAxisConstraints = axisConstraints.clean;
var axisIds = require('../plots/cartesian/axis_ids');


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

return Lib.syncOrAsync([
subroutines.layoutStyles,
drawAxes,
initInteractions
drawAxes
], gd);
}

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

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

// re-initialize cartesian interaction,
// which are sometimes cleared during marginPushers
seq = seq.concat(initInteractions);

return Lib.syncOrAsync(seq, gd);
return Lib.syncOrAsync([
marginPushers,
subroutines.layoutStyles
], gd);
}

function positionAndAutorange() {
if(!recalc) return;
if(!recalc) {
enforceAxisConstraints(gd);
return;
}

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

var axList = Plotly.Axes.list(gd, '', true);
for(var i = 0; i < axList.length; i++) {
Plotly.Axes.doAutoRange(axList[i]);
var ax = axList[i];
cleanAxisConstraints(gd, ax);

Plotly.Axes.doAutoRange(ax);
}

enforceAxisConstraints(gd);
Expand Down Expand Up @@ -365,11 +369,13 @@ Plotly.plot = function(gd, data, layout, config) {
drawFramework,
marginPushers,
marginPushersAgain,
initInteractions,
positionAndAutorange,
subroutines.layoutStyles,
drawAxes,
drawData,
finalDraw,
initInteractions,
Plots.rehover
];

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

function recordAlteredAxis(pleafPlus) {
var axId = axisIds.name2id(pleafPlus.split('.')[0]);
rangesAltered[axId] = 1;
return axId;
}

// alter gd.layout
Expand Down Expand Up @@ -1963,11 +1971,25 @@ function _relayout(gd, aobj) {
else if(pleafPlus.match(/^[xyz]axis[0-9]*\.range(\[[0|1]\])?$/)) {
doextra(ptrunk + '.autorange', false);
recordAlteredAxis(pleafPlus);
Lib.nestedProperty(fullLayout, ptrunk + '._inputRange').set(null);
}
else if(pleafPlus.match(/^[xyz]axis[0-9]*\.autorange$/)) {
doextra([ptrunk + '.range[0]', ptrunk + '.range[1]'],
undefined);
recordAlteredAxis(pleafPlus);
Lib.nestedProperty(fullLayout, ptrunk + '._inputRange').set(null);
var axFull = Lib.nestedProperty(fullLayout, ptrunk).get();
if(axFull._inputDomain) {
// if we're autoranging and this axis has a constrained domain,
// reset it so we don't get locked into a shrunken size
axFull._input.domain = axFull._inputDomain.slice();
}
}
else if(pleafPlus.match(/^[xyz]axis[0-9]*\.domain(\[[0|1]\])?$/)) {
Lib.nestedProperty(fullLayout, ptrunk + '._inputDomain').set(null);
}
else if(pleafPlus.match(/^[xyz]axis[0-9]*\.constrain.*$/)) {
flags.docalc = true;
}
else if(pleafPlus.match(/^aspectratio\.[xyz]$/)) {
doextra(proot + '.aspectmode', 'manual');
Expand Down Expand Up @@ -2047,6 +2069,7 @@ function _relayout(gd, aobj) {
// will not make sense, so autorange it.
doextra(ptrunk + '.autorange', true);
}
Lib.nestedProperty(fullLayout, ptrunk + '._inputRange').set(null);
}
else if(pleaf.match(cartesianConstants.AX_NAME_PATTERN)) {
var fullProp = Lib.nestedProperty(fullLayout, ai).get(),
Expand Down Expand Up @@ -2193,7 +2216,7 @@ function _relayout(gd, aobj) {

// figure out if we need to recalculate axis constraints
var constraints = fullLayout._axisConstraintGroups;
for(var axId in rangesAltered) {
for(axId in rangesAltered) {
for(i = 0; i < constraints.length; i++) {
var group = constraints[i];
if(group[axId]) {
Expand Down
21 changes: 18 additions & 3 deletions src/plot_api/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,11 @@ function crawl(objIn, objOut, schema, list, base, path) {
else if(!Lib.validate(valIn, nestedSchema)) {
list.push(format('value', base, p, valIn));
}
else if(nestedSchema.valType === 'enumerated' &&
((nestedSchema.coerceNumber && valIn !== +valOut) || valIn !== valOut)
) {
list.push(format('dynamic', base, p, valIn, valOut));
}
}

return list;
Expand Down Expand Up @@ -267,6 +272,16 @@ var code2msgFunc = {

return inBase(base) + target + ' ' + astr + ' did not get coerced';
},
dynamic: function(base, astr, valIn, valOut) {
return [
inBase(base) + 'key',
astr,
'(set to \'' + valIn + '\')',
'got reset to',
'\'' + valOut + '\'',
'during defaults.'
].join(' ');
},
invisible: function(base) {
return 'Trace ' + base[1] + ' got defaulted to be not visible';
},
Expand All @@ -284,7 +299,7 @@ function inBase(base) {
return 'In ' + base + ', ';
}

function format(code, base, path, valIn) {
function format(code, base, path, valIn, valOut) {
path = path || '';

var container, trace;
Expand All @@ -301,8 +316,8 @@ function format(code, base, path, valIn) {
trace = null;
}

var astr = convertPathToAttributeString(path),
msg = code2msgFunc[code](base, astr, valIn);
var astr = convertPathToAttributeString(path);
var msg = code2msgFunc[code](base, astr, valIn, valOut);

// log to console if logger config option is enabled
Lib.log(msg);
Expand Down
7 changes: 7 additions & 0 deletions src/plots/cartesian/axes.js
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,13 @@ axes.expand = function(ax, data, options) {
i, j, v, di, dmin, dmax,
ppadiplus, ppadiminus, includeThis, vmin, vmax;

// domain-constrained axes: base extrappad on the unconstrained
// domain so it's consistent as the domain changes
if(extrappad && (ax.constrain === 'domain') && ax._inputDomain) {
extrappad *= (ax._inputDomain[1] - ax._inputDomain[0]) /
(ax.domain[1] - ax.domain[0]);
}

function getPad(item) {
if(Array.isArray(item)) {
return function(i) { return Math.max(Number(item[i]||0), 0); };
Expand Down
21 changes: 18 additions & 3 deletions src/plots/cartesian/constraint_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,25 @@ var id2name = require('./axis_ids').id2name;

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

if(containerOut.fixedrange || !containerIn.scaleanchor) return;
if(containerOut.fixedrange) return;

var constraintOpts = getConstraintOpts(constraintGroups, containerOut._id, allAxisIds, layoutOut);
// coerce the constraint mechanics even if this axis has no scaleanchor
// because it may be the anchor of another axis.
coerce('constrain');
Lib.coerce(containerIn, containerOut, {
constraintoward: {
valType: 'enumerated',
values: letter === 'x' ? ['left', 'center', 'right'] : ['bottom', 'middle', 'top'],
dflt: letter === 'x' ? 'center' : 'middle'
}
}, 'constraintoward');

if(!containerIn.scaleanchor) return;

var constraintOpts = getConstraintOpts(constraintGroups, thisID, allAxisIds, layoutOut);

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

updateConstraintGroups(constraintGroups, constraintOpts.thisGroup,
containerOut._id, scaleanchor, scaleratio);
thisID, scaleanchor, scaleratio);
}
else if(allAxisIds.indexOf(containerIn.scaleanchor) !== -1) {
Lib.warn('ignored ' + containerOut._name + '.scaleanchor: "' +
Expand Down
Loading

0 comments on commit 51a479b

Please sign in to comment.