Skip to content

Commit 8c8dc1f

Browse files
committed
axis.constrain and axis.constraintoward
1 parent e37eeae commit 8c8dc1f

File tree

8 files changed

+265
-29
lines changed

8 files changed

+265
-29
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/plot_api/plot_api.js

+50-13
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,7 @@ Plotly.plot = function(gd, data, layout, config) {
190190

191191
return Lib.syncOrAsync([
192192
subroutines.layoutStyles,
193-
drawAxes,
194-
initInteractions
193+
drawAxes
195194
], gd);
196195
}
197196

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

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

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

234230
function positionAndAutorange() {
235-
if(!recalc) return;
231+
if(!recalc) {
232+
enforceAxisConstraints(gd);
233+
return;
234+
}
236235

237236
var subplots = Plots.getSubplotIds(fullLayout, 'cartesian'),
238237
modules = fullLayout._modules;
@@ -270,7 +269,26 @@ Plotly.plot = function(gd, data, layout, config) {
270269

271270
var axList = Plotly.Axes.list(gd, '', true);
272271
for(var i = 0; i < axList.length; i++) {
273-
Plotly.Axes.doAutoRange(axList[i]);
272+
// before autoranging, check if this axis was previously constrained
273+
// by domain but no longer is
274+
var ax = axList[i];
275+
if(ax._inputDomain) {
276+
var isConstrained = false;
277+
var axId = ax._id;
278+
var constraintGroups = gd._fullLayout._axisConstraintGroups;
279+
for(var j = 0; j < constraintGroups.length; j++) {
280+
if(constraintGroups[j][axId]) {
281+
isConstrained = true;
282+
break;
283+
}
284+
}
285+
if(!isConstrained || ax.constrain !== 'domain') {
286+
ax._input.domain = ax.domain = ax._inputDomain;
287+
delete ax._inputDomain;
288+
}
289+
}
290+
291+
Plotly.Axes.doAutoRange(ax);
274292
}
275293

276294
enforceAxisConstraints(gd);
@@ -370,6 +388,7 @@ Plotly.plot = function(gd, data, layout, config) {
370388
drawAxes,
371389
drawData,
372390
finalDraw,
391+
initInteractions,
373392
Plots.rehover
374393
];
375394

@@ -1913,10 +1932,12 @@ function _relayout(gd, aobj) {
19131932
// we're editing the (auto)range of, so we can tell the others constrained
19141933
// to scale with them that it's OK for them to shrink
19151934
var rangesAltered = {};
1935+
var axId;
19161936

19171937
function recordAlteredAxis(pleafPlus) {
19181938
var axId = axisIds.name2id(pleafPlus.split('.')[0]);
19191939
rangesAltered[axId] = 1;
1940+
return axId;
19201941
}
19211942

19221943
// alter gd.layout
@@ -1959,11 +1980,26 @@ function _relayout(gd, aobj) {
19591980
else if(pleafPlus.match(/^[xyz]axis[0-9]*\.range(\[[0|1]\])?$/)) {
19601981
doextra(ptrunk + '.autorange', false);
19611982
recordAlteredAxis(pleafPlus);
1983+
Lib.nestedProperty(fullLayout, ptrunk + '._inputRange').set(null);
19621984
}
19631985
else if(pleafPlus.match(/^[xyz]axis[0-9]*\.autorange$/)) {
19641986
doextra([ptrunk + '.range[0]', ptrunk + '.range[1]'],
19651987
undefined);
19661988
recordAlteredAxis(pleafPlus);
1989+
Lib.nestedProperty(fullLayout, ptrunk + '._inputRange').set(null);
1990+
var axFull = Lib.nestedProperty(fullLayout, ptrunk).get();
1991+
if(axFull._inputDomain) {
1992+
// if we're autoranging and this axis has a constrained domain,
1993+
// reset it so we don't get locked into a shrunken size
1994+
axFull._input.domain = axFull._inputDomain.slice();
1995+
}
1996+
}
1997+
else if(pleafPlus.match(/^[xyz]axis[0-9]*\.domain(\[[0|1]\])?$/)) {
1998+
axId = recordAlteredAxis(pleafPlus);
1999+
Lib.nestedProperty(fullLayout, ptrunk + '._inputDomain').set(null);
2000+
}
2001+
else if(pleafPlus.match(/^[xyz]axis[0-9]*\.constrain.*$/)) {
2002+
flags.docalc = true;
19672003
}
19682004
else if(pleafPlus.match(/^aspectratio\.[xyz]$/)) {
19692005
doextra(proot + '.aspectmode', 'manual');
@@ -2043,6 +2079,7 @@ function _relayout(gd, aobj) {
20432079
// will not make sense, so autorange it.
20442080
doextra(ptrunk + '.autorange', true);
20452081
}
2082+
Lib.nestedProperty(fullLayout, ptrunk + '._inputRange').set(null);
20462083
}
20472084
else if(pleaf.match(cartesianConstants.AX_NAME_PATTERN)) {
20482085
var fullProp = Lib.nestedProperty(fullLayout, ai).get(),
@@ -2189,7 +2226,7 @@ function _relayout(gd, aobj) {
21892226

21902227
// figure out if we need to recalculate axis constraints
21912228
var constraints = fullLayout._axisConstraintGroups;
2192-
for(var axId in rangesAltered) {
2229+
for(axId in rangesAltered) {
21932230
for(i = 0; i < constraints.length; i++) {
21942231
var group = constraints[i];
21952232
if(group[axId]) {

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._inputDomain) {
461+
extrappad *= (ax.domain[1] - ax.domain[0]) /
462+
(ax._inputDomain[1] - ax._inputDomain[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: "' +

src/plots/cartesian/constraints.js

+113-3
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@ var scaleZoom = require('./scale_zoom');
1414

1515
var ALMOST_EQUAL = require('../../constants/numerical').ALMOST_EQUAL;
1616

17+
var FROM_BL = require('../../constants/alignment').FROM_BL;
18+
1719

1820
module.exports = function enforceAxisConstraints(gd) {
1921
var fullLayout = gd._fullLayout;
2022
var constraintGroups = fullLayout._axisConstraintGroups;
2123

22-
var i, j, axisID, ax, normScale;
24+
var i, j, axisID, ax, normScale, mode, factor;
2325

2426
for(i = 0; i < constraintGroups.length; i++) {
2527
var group = constraintGroups[i];
@@ -41,6 +43,9 @@ module.exports = function enforceAxisConstraints(gd) {
4143
axisID = axisIDs[j];
4244
axes[axisID] = ax = fullLayout[id2name(axisID)];
4345

46+
if(!ax._inputDomain) ax._inputDomain = ax.domain.slice();
47+
if(!ax._inputRange) ax._inputRange = ax.range.slice();
48+
4449
// set axis scale here so we can use _m rather than
4550
// having to calculate it from length and range
4651
ax.setScale();
@@ -65,10 +70,115 @@ module.exports = function enforceAxisConstraints(gd) {
6570
for(j = 0; j < axisIDs.length; j++) {
6671
axisID = axisIDs[j];
6772
normScale = normScales[axisID];
73+
ax = axes[axisID];
74+
mode = ax.constrain;
75+
76+
// even if the scale didn't change, if we're shrinking domain
77+
// we need to recalculate in case `constraintoward` changed
78+
if(normScale !== matchScale || mode === 'domain') {
79+
factor = normScale / matchScale;
80+
81+
if(mode === 'range') {
82+
scaleZoom(ax, factor);
83+
}
84+
else {
85+
// mode === 'domain'
86+
87+
var inputDomain = ax._inputDomain;
88+
var domainShrunk = (ax.domain[1] - ax.domain[0]) /
89+
(inputDomain[1] - inputDomain[0]);
90+
var rangeShrunk = (ax.r2l(ax.range[1]) - ax.r2l(ax.range[0])) /
91+
(ax.r2l(ax._inputRange[1]) - ax.r2l(ax._inputRange[0]));
92+
93+
factor /= domainShrunk;
94+
95+
if(factor * rangeShrunk < 1) {
96+
// we've asked to magnify the axis more than we can just by
97+
// enlarging the domain - so we need to constrict range
98+
ax.domain = ax._input.domain = inputDomain.slice();
99+
scaleZoom(ax, factor);
100+
continue;
101+
}
102+
103+
if(rangeShrunk < 1) {
104+
// the range has previously been constricted by ^^, but we've
105+
// switched to the domain-constricted regime, so reset range
106+
ax.range = ax._input.range = ax._inputRange.slice();
107+
factor *= rangeShrunk;
108+
}
68109

69-
if(normScale !== matchScale) {
70-
scaleZoom(axes[axisID], normScale / matchScale);
110+
// TODO
111+
if(ax.autorange) {
112+
/*
113+
* range & factor may need to change because range was
114+
* calculated for the larger scaling, so some pixel
115+
* paddings may get cut off when we reduce the domain.
116+
*
117+
* This is easier than the regular autorange calculation
118+
* because we already know the scaling `m`, but we still
119+
* need to cut out impossible constraints (like
120+
* annotations with super-long arrows). That's what
121+
* outerMin/Max are for - if the expansion was going to
122+
* go beyond the original domain, it must be impossible
123+
*/
124+
var rangeMin = Math.min(ax.range[0], ax.range[1]);
125+
var rangeMax = Math.max(ax.range[0], ax.range[1]);
126+
var rangeCenter = (rangeMin + rangeMax) / 2;
127+
var halfRange = rangeMax - rangeCenter;
128+
var outerMin = rangeCenter - halfRange * factor;
129+
var outerMax = rangeCenter + halfRange * factor;
130+
131+
updateDomain(ax, factor);
132+
ax.setScale();
133+
var m = Math.abs(ax._m);
134+
var newVal;
135+
var k;
136+
137+
for(k = 0; k < ax._min.length; k++) {
138+
newVal = ax._min[i].val - ax._min[i].pad / m;
139+
if(newVal > outerMin && newVal < rangeMin) {
140+
rangeMin = newVal;
141+
}
142+
}
143+
144+
for(k = 0; k < ax._max.length; k++) {
145+
newVal = ax._max[i].val + ax._max[i].pad / m;
146+
if(newVal < outerMax && newVal > rangeMax) {
147+
rangeMax = newVal;
148+
}
149+
}
150+
151+
ax.range = ax._input.range = (ax.range[0] < ax.range[1]) ?
152+
[rangeMin, rangeMax] : [rangeMax, rangeMin];
153+
154+
/*
155+
* In principle this new range can be shifted vs. what
156+
* you saw at the end of a zoom operation, like if you
157+
* have a big bubble on one side and a small bubble on
158+
* the other.
159+
* To fix this we'd have to be doing this calculation
160+
* continuously during the zoom, but it's enough of an
161+
* edge case and a subtle enough effect that I'm going
162+
* to ignore it for now.
163+
*/
164+
var domainExpand = (rangeMax - rangeMin) / (2 * halfRange);
165+
factor /= domainExpand;
166+
}
167+
168+
updateDomain(ax, factor);
169+
}
71170
}
72171
}
73172
}
74173
};
174+
175+
function updateDomain(ax, factor) {
176+
var inputDomain = ax._inputDomain;
177+
var centerFraction = FROM_BL[ax.constraintoward];
178+
var center = inputDomain[0] + (inputDomain[1] - inputDomain[0]) * centerFraction;
179+
180+
ax.domain = ax._input.domain = [
181+
center + (inputDomain[0] - center) / factor,
182+
center + (inputDomain[1] - center) / factor
183+
];
184+
}

0 commit comments

Comments
 (0)