Skip to content

Commit 1f4898c

Browse files
committed
fix a bunch of edge cases in cartesian autorange
1 parent c87ccb3 commit 1f4898c

File tree

7 files changed

+152
-58
lines changed

7 files changed

+152
-58
lines changed

src/plots/cartesian/autorange.js

+63-49
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ function getAutoRange(gd, ax) {
8787
ax.autorange = true;
8888
}
8989

90+
var rangeMode = ax.rangemode;
91+
var toZero = rangeMode === 'tozero';
92+
var nonNegative = rangeMode === 'nonnegative';
93+
var axLen = ax._length;
94+
// don't allow padding to reduce the data to < 10% of the length
95+
var minSpan = axLen / 10;
96+
9097
var mbest = 0;
9198
var minpt, maxpt, minbest, maxbest, dp, dv;
9299

@@ -95,76 +102,83 @@ function getAutoRange(gd, ax) {
95102
for(j = 0; j < maxArray.length; j++) {
96103
maxpt = maxArray[j];
97104
dv = maxpt.val - minpt.val;
98-
dp = ax._length - getPad(minpt) - getPad(maxpt);
99-
if(dv > 0 && dp > 0 && dv / dp > mbest) {
100-
minbest = minpt;
101-
maxbest = maxpt;
102-
mbest = dv / dp;
105+
if(dv > 0) {
106+
dp = axLen - getPad(minpt) - getPad(maxpt);
107+
if(dp > minSpan) {
108+
if(dv / dp > mbest) {
109+
minbest = minpt;
110+
maxbest = maxpt;
111+
mbest = dv / dp;
112+
}
113+
}
114+
else if(dv / axLen > mbest) {
115+
// in case of padding longer than the axis
116+
// at least include the unpadded data values.
117+
minbest = {val: minpt.val, pad: 0};
118+
maxbest = {val: maxpt.val, pad: 0};
119+
mbest = dv / axLen;
120+
}
103121
}
104122
}
105123
}
106124

125+
function getMaxPad(prev, pt) {
126+
return Math.max(prev, getPad(pt));
127+
}
128+
107129
if(minmin === maxmax) {
108130
var lower = minmin - 1;
109131
var upper = minmin + 1;
110-
if(ax.rangemode === 'tozero') {
111-
newRange = minmin < 0 ? [lower, 0] : [0, upper];
112-
} else if(ax.rangemode === 'nonnegative') {
113-
newRange = [Math.max(0, lower), Math.max(0, upper)];
132+
if(toZero) {
133+
if(minmin === 0) {
134+
// The only value we have on this axis is 0, and we want to
135+
// autorange so zero is one end.
136+
// In principle this could be [0, 1] or [-1, 0] but usually
137+
// 'tozero' pins 0 to the low end, so follow that.
138+
newRange = [0, 1];
139+
}
140+
else {
141+
var maxPad = (minmin > 0 ? maxArray : minArray).reduce(getMaxPad, 0);
142+
// we're pushing a single value away from the edge due to its
143+
// padding, with the other end clamped at zero
144+
// 0.5 means don't push it farther than the center.
145+
var rangeEnd = minmin / (1 - Math.min(0.5, maxPad / axLen));
146+
newRange = minmin > 0 ? [0, rangeEnd] : [rangeEnd, 0];
147+
}
148+
} else if(nonNegative) {
149+
newRange = [Math.max(0, lower), Math.max(1, upper)];
114150
} else {
115151
newRange = [lower, upper];
116152
}
117153
}
118-
else if(mbest) {
119-
if(ax.type === 'linear' || ax.type === '-') {
120-
if(ax.rangemode === 'tozero') {
121-
if(minbest.val >= 0) {
122-
minbest = {val: 0, pad: 0};
123-
}
124-
if(maxbest.val <= 0) {
125-
maxbest = {val: 0, pad: 0};
126-
}
154+
else {
155+
if(toZero) {
156+
if(minbest.val >= 0) {
157+
minbest = {val: 0, pad: 0};
127158
}
128-
else if(ax.rangemode === 'nonnegative') {
129-
if(minbest.val - mbest * getPad(minbest) < 0) {
130-
minbest = {val: 0, pad: 0};
131-
}
132-
if(maxbest.val < 0) {
133-
maxbest = {val: 1, pad: 0};
134-
}
159+
if(maxbest.val <= 0) {
160+
maxbest = {val: 0, pad: 0};
161+
}
162+
}
163+
else if(nonNegative) {
164+
if(minbest.val - mbest * getPad(minbest) < 0) {
165+
minbest = {val: 0, pad: 0};
166+
}
167+
if(maxbest.val <= 0) {
168+
maxbest = {val: 1, pad: 0};
135169
}
136-
137-
// in case it changed again...
138-
mbest = (maxbest.val - minbest.val) /
139-
(ax._length - getPad(minbest) - getPad(maxbest));
140-
141170
}
142171

172+
// in case it changed again...
173+
mbest = (maxbest.val - minbest.val) /
174+
(axLen - getPad(minbest) - getPad(maxbest));
175+
143176
newRange = [
144177
minbest.val - mbest * getPad(minbest),
145178
maxbest.val + mbest * getPad(maxbest)
146179
];
147180
}
148181

149-
// don't let axis have zero size, while still respecting tozero and nonnegative
150-
if(newRange[0] === newRange[1]) {
151-
if(ax.rangemode === 'tozero') {
152-
if(newRange[0] < 0) {
153-
newRange = [newRange[0], 0];
154-
} else if(newRange[0] > 0) {
155-
newRange = [0, newRange[0]];
156-
} else {
157-
newRange = [0, 1];
158-
}
159-
}
160-
else {
161-
newRange = [newRange[0] - 1, newRange[0] + 1];
162-
if(ax.rangemode === 'nonnegative') {
163-
newRange[0] = Math.max(0, newRange[0]);
164-
}
165-
}
166-
}
167-
168182
// maintain reversal
169183
if(axReverse) newRange.reverse();
170184

src/plots/polar/layout_defaults.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ function handleDefaults(contIn, contOut, coerce, opts) {
9191
case 'radialaxis':
9292
var autoRange = coerceAxis('autorange', !axOut.isValidRange(axIn.range));
9393
axIn.autorange = autoRange;
94-
if(autoRange) coerceAxis('rangemode');
94+
if(autoRange && (axType === 'linear' || axType === '-')) coerceAxis('rangemode');
9595
if(autoRange === 'reversed') axOut._m = -1;
9696

9797
coerceAxis('range');
697 Bytes
Loading
-1.7 KB
Loading

test/image/baselines/contour_log.png

-28 Bytes
Loading

test/image/mocks/scatter_fill_corner_cases.json

-2
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@
7575
{
7676
"x": [1.5],
7777
"y": [1.25],
78-
"fill": "tonexty",
7978
"showlegend": false,
8079
"yaxis": "y2"
8180
},
@@ -111,7 +110,6 @@
111110
{
112111
"x": [1.5],
113112
"y": [1.25],
114-
"fill": "tonexty",
115113
"line": {"shape": "spline"},
116114
"xaxis": "x2",
117115
"showlegend": false,

test/jasmine/tests/axes_test.js

+88-6
Original file line numberDiff line numberDiff line change
@@ -1558,7 +1558,7 @@ describe('Test axes', function() {
15581558
expect(getAutoRange(gd, ax)).toEqual([7.5, 0]);
15591559
});
15601560

1561-
it('expands empty positive range to something including 0 with rangemode tozero', function() {
1561+
it('expands empty positive range to include 0 with rangemode tozero', function() {
15621562
gd = mockGd([
15631563
{val: 5, pad: 0}
15641564
], [
@@ -1567,7 +1567,7 @@ describe('Test axes', function() {
15671567
ax = mockAx();
15681568
ax.rangemode = 'tozero';
15691569

1570-
expect(getAutoRange(gd, ax)).toEqual([0, 6]);
1570+
expect(getAutoRange(gd, ax)).toEqual([0, 5]);
15711571
});
15721572

15731573
it('expands empty negative range to something including 0 with rangemode tozero', function() {
@@ -1579,7 +1579,63 @@ describe('Test axes', function() {
15791579
ax = mockAx();
15801580
ax.rangemode = 'tozero';
15811581

1582-
expect(getAutoRange(gd, ax)).toEqual([-6, 0]);
1582+
expect(getAutoRange(gd, ax)).toEqual([-5, 0]);
1583+
});
1584+
1585+
it('pads an empty range, but not past center, with rangemode tozero', function() {
1586+
gd = mockGd([
1587+
{val: 5, pad: 50} // this min pad gets ignored
1588+
], [
1589+
{val: 5, pad: 20}
1590+
]);
1591+
ax = mockAx();
1592+
ax.rangemode = 'tozero';
1593+
1594+
expect(getAutoRange(gd, ax)).toBeCloseToArray([0, 6.25], 0.01);
1595+
1596+
gd = mockGd([
1597+
{val: -5, pad: 80}
1598+
], [
1599+
{val: -5, pad: 0}
1600+
]);
1601+
ax = mockAx();
1602+
ax.rangemode = 'tozero';
1603+
1604+
expect(getAutoRange(gd, ax)).toBeCloseToArray([-10, 0], 0.01);
1605+
});
1606+
1607+
it('shows the data even if it cannot show the padding', function() {
1608+
gd = mockGd([
1609+
{val: 0, pad: 44}
1610+
], [
1611+
{val: 1, pad: 44}
1612+
]);
1613+
ax = mockAx();
1614+
1615+
// this one is *just* on the allowed side of padding
1616+
// ie data span is just over 10% of the axis
1617+
expect(getAutoRange(gd, ax)).toBeCloseToArray([-3.67, 4.67]);
1618+
1619+
gd = mockGd([
1620+
{val: 0, pad: 46}
1621+
], [
1622+
{val: 1, pad: 46}
1623+
]);
1624+
ax = mockAx();
1625+
1626+
// this one the padded data span would be too small, so we delete
1627+
// the padding
1628+
expect(getAutoRange(gd, ax)).toEqual([0, 1]);
1629+
1630+
gd = mockGd([
1631+
{val: 0, pad: 400}
1632+
], [
1633+
{val: 1, pad: 0}
1634+
]);
1635+
ax = mockAx();
1636+
1637+
// this one the padding is simply impossible to accept!
1638+
expect(getAutoRange(gd, ax)).toEqual([0, 1]);
15831639
});
15841640

15851641
it('never returns a negative range when rangemode nonnegative is set with positive and negative points', function() {
@@ -1614,17 +1670,43 @@ describe('Test axes', function() {
16141670
expect(getAutoRange(gd, ax)).toEqual([0, 1]);
16151671
});
16161672

1617-
it('expands empty range to something nonnegative with rangemode nonnegative', function() {
1673+
it('never returns a negative range when rangemode nonnegative is set with only nonpositive points', function() {
16181674
gd = mockGd([
1619-
{val: -5, pad: 0}
1675+
{val: -10, pad: 20},
1676+
{val: -8, pad: 0},
1677+
{val: -9, pad: 10}
16201678
], [
1621-
{val: -5, pad: 0}
1679+
{val: -5, pad: 20},
1680+
{val: 0, pad: 0},
1681+
{val: -6, pad: 10}
16221682
]);
16231683
ax = mockAx();
16241684
ax.rangemode = 'nonnegative';
16251685

16261686
expect(getAutoRange(gd, ax)).toEqual([0, 1]);
16271687
});
1688+
1689+
it('expands empty range to something nonnegative with rangemode nonnegative', function() {
1690+
[
1691+
[-5, [0, 1]],
1692+
[0, [0, 1]],
1693+
[0.5, [0, 1.5]],
1694+
[1, [0, 2]],
1695+
[5, [4, 6]]
1696+
].forEach(function(testCase) {
1697+
var val = testCase[0];
1698+
var expected = testCase[1];
1699+
gd = mockGd([
1700+
{val: val, pad: 0}
1701+
], [
1702+
{val: val, pad: 0}
1703+
]);
1704+
ax = mockAx();
1705+
ax.rangemode = 'nonnegative';
1706+
1707+
expect(getAutoRange(gd, ax)).toEqual(expected, val);
1708+
});
1709+
});
16281710
});
16291711

16301712
describe('findExtremes', function() {

0 commit comments

Comments
 (0)