Skip to content

Commit 846fde4

Browse files
committed
More precise choice of axes limits.
plt.plot([-.1, .2]) used to pick (in round numbers mode) [-.1, .25] as ylims due to floating point inaccuracies; change it to pick [-.1, .2] (up to floating point inaccuracies). Support for the (unused and deprecated-in-comment) "trim" keyword argument has been dropped as the way ticks are picked has changed. Many test images have changed! Implementation notes: - A bug in numpy's implementation of divmod (numpy/numpy#6127) is worked around. - The implementation of scale_range has also been cleaned. See #5767, #5738.
1 parent 0abca30 commit 846fde4

23 files changed

+3490
-3593
lines changed
Binary file not shown.

lib/matplotlib/tests/baseline_images/test_axes/autoscale_tiny_range.svg

+194-242
Loading
Binary file not shown.

lib/matplotlib/tests/baseline_images/test_axes/errorbar_mixed.svg

+506-506
Loading
Binary file not shown.

lib/matplotlib/tests/baseline_images/test_axes/formatter_large_small.svg

+107-127
Loading
Binary file not shown.

lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_tight_raster.svg

+30-42
Loading

lib/matplotlib/tests/test_axes.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from matplotlib.testing.decorators import image_comparison, cleanup
2222
import matplotlib.pyplot as plt
2323
import matplotlib.markers as mmarkers
24-
from numpy.testing import assert_array_equal
24+
from numpy.testing import assert_allclose, assert_array_equal
2525
import warnings
2626
from matplotlib.cbook import IgnoredKeywordWarning
2727

@@ -3708,8 +3708,7 @@ def test_vline_limit():
37083708
ax.axvline(0.5)
37093709
ax.plot([-0.1, 0, 0.2, 0.1])
37103710
(ymin, ymax) = ax.get_ylim()
3711-
assert ymin == -0.1
3712-
assert ymax == 0.25
3711+
assert_allclose(ax.get_ylim(), (-.1, .2))
37133712

37143713

37153714
@cleanup

lib/matplotlib/ticker.py

+38-38
Original file line numberDiff line numberDiff line change
@@ -161,13 +161,23 @@
161161
from matplotlib import rcParams
162162
from matplotlib import cbook
163163
from matplotlib import transforms as mtransforms
164+
from matplotlib.cbook import mplDeprecation
164165

165166
import warnings
166167

167168
if six.PY3:
168169
long = int
169170

170171

172+
# Work around numpy/numpy#6127.
173+
def _divmod(x, y):
174+
if isinstance(x, np.generic):
175+
x = x.item()
176+
if isinstance(y, np.generic):
177+
y = y.item()
178+
return six.moves.builtins.divmod(x, y)
179+
180+
171181
def _mathdefault(s):
172182
"""
173183
For backward compatibility, in classic mode we display
@@ -1218,7 +1228,7 @@ def view_limits(self, vmin, vmax):
12181228
vmax += 1
12191229

12201230
if rcParams['axes.autolimit_mode'] == 'round_numbers':
1221-
exponent, remainder = divmod(math.log10(vmax - vmin), 1)
1231+
exponent, remainder = _divmod(math.log10(vmax - vmin), 1)
12221232
if remainder < 0.5:
12231233
exponent -= 1
12241234
scale = 10 ** (-exponent)
@@ -1244,30 +1254,30 @@ def __init__(self, base):
12441254

12451255
def lt(self, x):
12461256
'return the largest multiple of base < x'
1247-
d, m = divmod(x, self._base)
1257+
d, m = _divmod(x, self._base)
12481258
if closeto(m, 0) and not closeto(m / self._base, 1):
12491259
return (d - 1) * self._base
12501260
return d * self._base
12511261

12521262
def le(self, x):
12531263
'return the largest multiple of base <= x'
1254-
d, m = divmod(x, self._base)
1264+
d, m = _divmod(x, self._base)
12551265
if closeto(m / self._base, 1): # was closeto(m, self._base)
12561266
#looks like floating point error
12571267
return (d + 1) * self._base
12581268
return d * self._base
12591269

12601270
def gt(self, x):
12611271
'return the smallest multiple of base > x'
1262-
d, m = divmod(x, self._base)
1272+
d, m = _divmod(x, self._base)
12631273
if closeto(m / self._base, 1):
12641274
#looks like floating point error
12651275
return (d + 2) * self._base
12661276
return (d + 1) * self._base
12671277

12681278
def ge(self, x):
12691279
'return the smallest multiple of base >= x'
1270-
d, m = divmod(x, self._base)
1280+
d, m = _divmod(x, self._base)
12711281
if closeto(m, 0) and not closeto(m / self._base, 1):
12721282
return d * self._base
12731283
return (d + 1) * self._base
@@ -1323,23 +1333,13 @@ def view_limits(self, dmin, dmax):
13231333

13241334

13251335
def scale_range(vmin, vmax, n=1, threshold=100):
1326-
dv = abs(vmax - vmin)
1327-
if dv == 0: # maxabsv == 0 is a special case of this.
1328-
return 1.0, 0.0
1329-
# Note: this should never occur because
1330-
# vmin, vmax should have been checked by nonsingular(),
1331-
# and spread apart if necessary.
1332-
meanv = 0.5 * (vmax + vmin)
1336+
dv = abs(vmax - vmin) # > 0 as nonsingular is called before.
1337+
meanv = (vmax + vmin) / 2
13331338
if abs(meanv) / dv < threshold:
13341339
offset = 0
1335-
elif meanv > 0:
1336-
ex = divmod(math.log10(meanv), 1)[0]
1337-
offset = 10 ** ex
13381340
else:
1339-
ex = divmod(math.log10(-meanv), 1)[0]
1340-
offset = -10 ** ex
1341-
ex = divmod(math.log10(dv / n), 1)[0]
1342-
scale = 10 ** ex
1341+
offset = math.copysign(10 ** (math.log10(abs(meanv)) // 1), meanv)
1342+
scale = 10 ** (math.log10(dv / n) // 1)
13431343
return scale, offset
13441344

13451345

@@ -1349,7 +1349,6 @@ class MaxNLocator(Locator):
13491349
"""
13501350
default_params = dict(nbins=10,
13511351
steps=None,
1352-
trim=True,
13531352
integer=False,
13541353
symmetric=False,
13551354
prune=None)
@@ -1385,9 +1384,6 @@ def __init__(self, *args, **kwargs):
13851384
will be removed. If prune==None, no ticks will be removed.
13861385
13871386
"""
1388-
# I left "trim" out; it defaults to True, and it is not
1389-
# clear that there is any use case for False, so we may
1390-
# want to remove that kwarg. EF 2010/04/18
13911387
if args:
13921388
kwargs['nbins'] = args[0]
13931389
if len(args) > 1:
@@ -1403,7 +1399,8 @@ def set_params(self, **kwargs):
14031399
if self._nbins != 'auto':
14041400
self._nbins = int(self._nbins)
14051401
if 'trim' in kwargs:
1406-
self._trim = kwargs['trim']
1402+
warnings.warn("The 'trim' keyword has no effect anymore",
1403+
mplDeprecation)
14071404
if 'integer' in kwargs:
14081405
self._integer = kwargs['integer']
14091406
if 'symmetric' in kwargs:
@@ -1426,9 +1423,9 @@ def set_params(self, **kwargs):
14261423
if 'integer' in kwargs:
14271424
self._integer = kwargs['integer']
14281425
if self._integer:
1429-
self._steps = [n for n in self._steps if divmod(n, 1)[1] < 0.001]
1426+
self._steps = [n for n in self._steps if _divmod(n, 1)[1] < 0.001]
14301427

1431-
def bin_boundaries(self, vmin, vmax):
1428+
def _raw_ticks(self, vmin, vmax):
14321429
nbins = self._nbins
14331430
if nbins == 'auto':
14341431
nbins = self.axis.get_tick_space()
@@ -1446,23 +1443,26 @@ def bin_boundaries(self, vmin, vmax):
14461443
if step < scaled_raw_step:
14471444
continue
14481445
step *= scale
1449-
best_vmin = step * divmod(vmin, step)[0]
1446+
best_vmin = vmin // step * step
14501447
best_vmax = best_vmin + step * nbins
1451-
if (best_vmax >= vmax):
1448+
if best_vmax >= vmax:
14521449
break
1453-
if self._trim:
1454-
extra_bins = int(divmod((best_vmax - vmax), step)[0])
1455-
nbins -= extra_bins
1456-
return (np.arange(nbins + 1) * step + best_vmin + offset)
1450+
1451+
# More than nbins may be required, e.g. vmin, vmax = -4.1, 4.1 gives
1452+
# nbins=9 but 10 bins are actually required after rounding. So we just
1453+
# create the bins that span the range we need instead.
1454+
low = round(Base(step).le(vmin - best_vmin) / step)
1455+
high = round(Base(step).ge(vmax - best_vmin) / step)
1456+
return np.arange(low, high + 1) * step + best_vmin + offset
14571457

14581458
def __call__(self):
14591459
vmin, vmax = self.axis.get_view_interval()
14601460
return self.tick_values(vmin, vmax)
14611461

14621462
def tick_values(self, vmin, vmax):
1463-
vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=1e-13,
1464-
tiny=1e-14)
1465-
locs = self.bin_boundaries(vmin, vmax)
1463+
vmin, vmax = mtransforms.nonsingular(
1464+
vmin, vmax, expander=1e-13, tiny=1e-14)
1465+
locs = self._raw_ticks(vmin, vmax)
14661466
prune = self._prune
14671467
if prune == 'lower':
14681468
locs = locs[1:]
@@ -1479,11 +1479,11 @@ def view_limits(self, dmin, dmax):
14791479
dmin = -maxabs
14801480
dmax = maxabs
14811481

1482-
dmin, dmax = mtransforms.nonsingular(dmin, dmax, expander=1e-12,
1483-
tiny=1.e-13)
1482+
dmin, dmax = mtransforms.nonsingular(
1483+
dmin, dmax, expander=1e-12, tiny=1e-13)
14841484

14851485
if rcParams['axes.autolimit_mode'] == 'round_numbers':
1486-
return np.take(self.bin_boundaries(dmin, dmax), [0, -1])
1486+
return self._raw_ticks(dmin, dmax)[[0, -1]]
14871487
else:
14881488
return dmin, dmax
14891489

Binary file not shown.

0 commit comments

Comments
 (0)