Skip to content

Commit b19a87f

Browse files
authored
Merge pull request #1522 from plotly/axis-constraints
Axis constraints
2 parents 7f63e29 + 23859e3 commit b19a87f

29 files changed

+1933
-608
lines changed

src/components/colorbar/draw.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,10 @@ module.exports = function draw(gd, id) {
170170
anchor: 'free',
171171
position: 1
172172
},
173-
cbAxisOut = {},
173+
cbAxisOut = {
174+
type: 'linear',
175+
_id: 'y' + id
176+
},
174177
axisOptions = {
175178
letter: 'y',
176179
font: fullLayout.font,
@@ -188,8 +191,6 @@ module.exports = function draw(gd, id) {
188191
handleAxisDefaults(cbAxisIn, cbAxisOut, coerce, axisOptions, fullLayout);
189192
handleAxisPositionDefaults(cbAxisIn, cbAxisOut, coerce, axisOptions);
190193

191-
cbAxisOut._id = 'y' + id;
192-
193194
// position can't go in through supplyDefaults
194195
// because that restricts it to [0,1]
195196
cbAxisOut.position = opts.x + xpadFrac + thickFrac;

src/constants/numerical.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,10 @@ module.exports = {
4242
* For fast conversion btwn world calendars and epoch ms, the Julian Day Number
4343
* of the unix epoch. From calendars.instance().newDate(1970, 1, 1).toJD()
4444
*/
45-
EPOCHJD: 2440587.5
45+
EPOCHJD: 2440587.5,
46+
47+
/*
48+
* Are two values nearly equal? Compare to 1PPM
49+
*/
50+
ALMOST_EQUAL: 1 - 1e-6
4651
};

src/plot_api/plot_api.js

+61-21
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ 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');
36+
var axisIds = require('../plots/cartesian/axis_ids');
3537

3638

3739
/**
@@ -151,10 +153,6 @@ Plotly.plot = function(gd, data, layout, config) {
151153
makePlotFramework(gd);
152154
}
153155

154-
// save initial axis range once per graph
155-
if(graphWasEmpty) Plotly.Axes.saveRangeInitial(gd);
156-
157-
158156
// prepare the data and find the autorange
159157

160158
// generate calcdata, if we need to
@@ -256,18 +254,24 @@ Plotly.plot = function(gd, data, layout, config) {
256254
return Lib.syncOrAsync([
257255
Registry.getComponentMethod('shapes', 'calcAutorange'),
258256
Registry.getComponentMethod('annotations', 'calcAutorange'),
259-
doAutoRange,
257+
doAutoRangeAndConstraints,
260258
Registry.getComponentMethod('rangeslider', 'calcAutorange')
261259
], gd);
262260
}
263261

264-
function doAutoRange() {
262+
function doAutoRangeAndConstraints() {
265263
if(gd._transitioning) return;
266264

267265
var axList = Plotly.Axes.list(gd, '', true);
268266
for(var i = 0; i < axList.length; i++) {
269267
Plotly.Axes.doAutoRange(axList[i]);
270268
}
269+
270+
enforceAxisConstraints(gd);
271+
272+
// store initial ranges *after* enforcing constraints, otherwise
273+
// we will never look like we're at the initial ranges
274+
if(graphWasEmpty) Plotly.Axes.saveRangeInitial(gd);
271275
}
272276

273277
// draw ticks, titles, and calculate axis scaling (._b, ._m)
@@ -1863,6 +1867,16 @@ function _relayout(gd, aobj) {
18631867
return (ax || {}).autorange;
18641868
}
18651869

1870+
// for constraint enforcement: keep track of all axes (as {id: name})
1871+
// we're editing the (auto)range of, so we can tell the others constrained
1872+
// to scale with them that it's OK for them to shrink
1873+
var rangesAltered = {};
1874+
1875+
function recordAlteredAxis(pleafPlus) {
1876+
var axId = axisIds.name2id(pleafPlus.split('.')[0]);
1877+
rangesAltered[axId] = 1;
1878+
}
1879+
18661880
// alter gd.layout
18671881
for(var ai in aobj) {
18681882
if(helpers.hasParent(aobj, ai)) {
@@ -1897,15 +1911,17 @@ function _relayout(gd, aobj) {
18971911
//
18981912
// To do so, we must manually set them back here using the _initialAutoSize cache.
18991913
if(['width', 'height'].indexOf(ai) !== -1 && vi === null) {
1900-
gd._fullLayout[ai] = gd._initialAutoSize[ai];
1914+
fullLayout[ai] = gd._initialAutoSize[ai];
19011915
}
19021916
// check autorange vs range
19031917
else if(pleafPlus.match(/^[xyz]axis[0-9]*\.range(\[[0|1]\])?$/)) {
19041918
doextra(ptrunk + '.autorange', false);
1919+
recordAlteredAxis(pleafPlus);
19051920
}
19061921
else if(pleafPlus.match(/^[xyz]axis[0-9]*\.autorange$/)) {
19071922
doextra([ptrunk + '.range[0]', ptrunk + '.range[1]'],
19081923
undefined);
1924+
recordAlteredAxis(pleafPlus);
19091925
}
19101926
else if(pleafPlus.match(/^aspectratio\.[xyz]$/)) {
19111927
doextra(proot + '.aspectmode', 'manual');
@@ -2069,6 +2085,18 @@ function _relayout(gd, aobj) {
20692085
else if(proot.indexOf('geo') === 0) flags.doplot = true;
20702086
else if(proot.indexOf('ternary') === 0) flags.doplot = true;
20712087
else if(ai === 'paper_bgcolor') flags.doplot = true;
2088+
else if(proot === 'margin' ||
2089+
pp1 === 'autorange' ||
2090+
pp1 === 'rangemode' ||
2091+
pp1 === 'type' ||
2092+
pp1 === 'domain' ||
2093+
pp1 === 'fixedrange' ||
2094+
pp1 === 'scaleanchor' ||
2095+
pp1 === 'scaleratio' ||
2096+
ai.indexOf('calendar') !== -1 ||
2097+
ai.match(/^(bar|box|font)/)) {
2098+
flags.docalc = true;
2099+
}
20722100
else if(fullLayout._has('gl2d') &&
20732101
(ai.indexOf('axis') !== -1 || ai === 'plot_bgcolor')
20742102
) flags.doplot = true;
@@ -2092,15 +2120,6 @@ function _relayout(gd, aobj) {
20922120
else if(ai === 'margin.pad') {
20932121
flags.doticks = flags.dolayoutstyle = true;
20942122
}
2095-
else if(proot === 'margin' ||
2096-
pp1 === 'autorange' ||
2097-
pp1 === 'rangemode' ||
2098-
pp1 === 'type' ||
2099-
pp1 === 'domain' ||
2100-
ai.indexOf('calendar') !== -1 ||
2101-
ai.match(/^(bar|box|font)/)) {
2102-
flags.docalc = true;
2103-
}
21042123
/*
21052124
* hovermode and dragmode don't need any redrawing, since they just
21062125
* affect reaction to user input, everything else, assume full replot.
@@ -2124,16 +2143,37 @@ function _relayout(gd, aobj) {
21242143
if(!finished) flags.doplot = true;
21252144
}
21262145

2127-
var oldWidth = gd._fullLayout.width,
2128-
oldHeight = gd._fullLayout.height;
2146+
// figure out if we need to recalculate axis constraints
2147+
var constraints = fullLayout._axisConstraintGroups;
2148+
for(var axId in rangesAltered) {
2149+
for(i = 0; i < constraints.length; i++) {
2150+
var group = constraints[i];
2151+
if(group[axId]) {
2152+
// Always recalc if we're changing constrained ranges.
2153+
// Otherwise it's possible to violate the constraints by
2154+
// specifying arbitrary ranges for all axes in the group.
2155+
// this way some ranges may expand beyond what's specified,
2156+
// as they do at first draw, to satisfy the constraints.
2157+
flags.docalc = true;
2158+
for(var groupAxId in group) {
2159+
if(!rangesAltered[groupAxId]) {
2160+
axisIds.getFromId(gd, groupAxId)._constraintShrinkable = true;
2161+
}
2162+
}
2163+
}
2164+
}
2165+
}
2166+
2167+
var oldWidth = fullLayout.width,
2168+
oldHeight = fullLayout.height;
21292169

21302170
// calculate autosizing
2131-
if(gd.layout.autosize) Plots.plotAutoSize(gd, gd.layout, gd._fullLayout);
2171+
if(gd.layout.autosize) Plots.plotAutoSize(gd, gd.layout, fullLayout);
21322172

21332173
// avoid unnecessary redraws
21342174
var hasSizechanged = aobj.height || aobj.width ||
2135-
(gd._fullLayout.width !== oldWidth) ||
2136-
(gd._fullLayout.height !== oldHeight);
2175+
(fullLayout.width !== oldWidth) ||
2176+
(fullLayout.height !== oldHeight);
21372177

21382178
if(hasSizechanged) flags.docalc = true;
21392179

src/plots/cartesian/axis_defaults.js

+2-110
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,18 @@ var handleTickLabelDefaults = require('./tick_label_defaults');
2222
var handleCategoryOrderDefaults = require('./category_order_defaults');
2323
var setConvert = require('./set_convert');
2424
var orderedCategories = require('./ordered_categories');
25-
var axisIds = require('./axis_ids');
26-
var autoType = require('./axis_autotype');
2725

2826

2927
/**
3028
* options: object containing:
3129
*
3230
* letter: 'x' or 'y'
3331
* title: name of the axis (ie 'Colorbar') to go in default title
34-
* name: axis object name (ie 'xaxis') if one should be stored
3532
* font: the default font to inherit
3633
* outerTicks: boolean, should ticks default to outside?
3734
* showGrid: boolean, should gridlines be shown by default?
3835
* noHover: boolean, this axis doesn't support hover effects?
39-
* data: the plot data to use in choosing auto type
36+
* data: the plot data, used to manage categories
4037
* bgColor: the plot background color, to calculate default gridline colors
4138
*/
4239
module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, options, layoutOut) {
@@ -50,28 +47,7 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,
5047
return Lib.coerce2(containerIn, containerOut, layoutAttributes, attr, dflt);
5148
}
5249

53-
// set up some private properties
54-
if(options.name) {
55-
containerOut._name = options.name;
56-
containerOut._id = axisIds.name2id(options.name);
57-
}
58-
59-
// now figure out type and do some more initialization
60-
var axType = coerce('type');
61-
if(axType === '-') {
62-
setAutoType(containerOut, options.data);
63-
64-
if(containerOut.type === '-') {
65-
containerOut.type = 'linear';
66-
}
67-
else {
68-
// copy autoType back to input axis
69-
// note that if this object didn't exist
70-
// in the input layout, we have to put it in
71-
// this happens in the main supplyDefaults function
72-
axType = containerIn.type = containerOut.type;
73-
}
74-
}
50+
var axType = containerOut.type;
7551

7652
if(axType === 'date') {
7753
var handleCalendarDefaults = Registry.getComponentMethod('calendars', 'handleDefaults');
@@ -140,87 +116,3 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,
140116

141117
return containerOut;
142118
};
143-
144-
function setAutoType(ax, data) {
145-
// new logic: let people specify any type they want,
146-
// only autotype if type is '-'
147-
if(ax.type !== '-') return;
148-
149-
var id = ax._id,
150-
axLetter = id.charAt(0);
151-
152-
// support 3d
153-
if(id.indexOf('scene') !== -1) id = axLetter;
154-
155-
var d0 = getFirstNonEmptyTrace(data, id, axLetter);
156-
if(!d0) return;
157-
158-
// first check for histograms, as the count direction
159-
// should always default to a linear axis
160-
if(d0.type === 'histogram' &&
161-
axLetter === {v: 'y', h: 'x'}[d0.orientation || 'v']) {
162-
ax.type = 'linear';
163-
return;
164-
}
165-
166-
var calAttr = axLetter + 'calendar',
167-
calendar = d0[calAttr];
168-
169-
// check all boxes on this x axis to see
170-
// if they're dates, numbers, or categories
171-
if(isBoxWithoutPositionCoords(d0, axLetter)) {
172-
var posLetter = getBoxPosLetter(d0),
173-
boxPositions = [],
174-
trace;
175-
176-
for(var i = 0; i < data.length; i++) {
177-
trace = data[i];
178-
if(!Registry.traceIs(trace, 'box') ||
179-
(trace[axLetter + 'axis'] || axLetter) !== id) continue;
180-
181-
if(trace[posLetter] !== undefined) boxPositions.push(trace[posLetter][0]);
182-
else if(trace.name !== undefined) boxPositions.push(trace.name);
183-
else boxPositions.push('text');
184-
185-
if(trace[calAttr] !== calendar) calendar = undefined;
186-
}
187-
188-
ax.type = autoType(boxPositions, calendar);
189-
}
190-
else {
191-
ax.type = autoType(d0[axLetter] || [d0[axLetter + '0']], calendar);
192-
}
193-
}
194-
195-
function getBoxPosLetter(trace) {
196-
return {v: 'x', h: 'y'}[trace.orientation || 'v'];
197-
}
198-
199-
function isBoxWithoutPositionCoords(trace, axLetter) {
200-
var posLetter = getBoxPosLetter(trace),
201-
isBox = Registry.traceIs(trace, 'box'),
202-
isCandlestick = Registry.traceIs(trace._fullInput || {}, 'candlestick');
203-
204-
return (
205-
isBox &&
206-
!isCandlestick &&
207-
axLetter === posLetter &&
208-
trace[posLetter] === undefined &&
209-
trace[posLetter + '0'] === undefined
210-
);
211-
}
212-
213-
function getFirstNonEmptyTrace(data, id, axLetter) {
214-
for(var i = 0; i < data.length; i++) {
215-
var trace = data[i];
216-
217-
if((trace[axLetter + 'axis'] || axLetter) === id) {
218-
if(isBoxWithoutPositionCoords(trace, axLetter)) {
219-
return trace;
220-
}
221-
else if((trace[axLetter] || []).length || trace[axLetter + '0']) {
222-
return trace;
223-
}
224-
}
225-
}
226-
}

0 commit comments

Comments
 (0)