Skip to content

Commit e67c7fc

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 8e38a54 commit e67c7fc

23 files changed

+3495
-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
@@ -22,7 +22,7 @@
2222
from matplotlib.testing.decorators import image_comparison, cleanup
2323
import matplotlib.pyplot as plt
2424
import matplotlib.markers as mmarkers
25-
from numpy.testing import assert_array_equal
25+
from numpy.testing import assert_allclose, assert_array_equal
2626
import warnings
2727
from matplotlib.cbook import IgnoredKeywordWarning
2828

@@ -3715,8 +3715,7 @@ def test_vline_limit():
37153715
ax.axvline(0.5)
37163716
ax.plot([-0.1, 0, 0.2, 0.1])
37173717
(ymin, ymax) = ax.get_ylim()
3718-
assert ymin == -0.1
3719-
assert ymax == 0.25
3718+
assert_allclose(ax.get_ylim(), (-.1, .2))
37203719

37213720

37223721
@cleanup

lib/matplotlib/ticker.py

+43-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
@@ -1221,7 +1231,7 @@ def view_limits(self, vmin, vmax):
12211231
vmax += 1
12221232

12231233
if rcParams['axes.autolimit_mode'] == 'round_numbers':
1224-
exponent, remainder = divmod(math.log10(vmax - vmin), 1)
1234+
exponent, remainder = _divmod(math.log10(vmax - vmin), 1)
12251235
if remainder < 0.5:
12261236
exponent -= 1
12271237
scale = 10 ** (-exponent)
@@ -1247,30 +1257,30 @@ def __init__(self, base):
12471257

12481258
def lt(self, x):
12491259
'return the largest multiple of base < x'
1250-
d, m = divmod(x, self._base)
1260+
d, m = _divmod(x, self._base)
12511261
if closeto(m, 0) and not closeto(m / self._base, 1):
12521262
return (d - 1) * self._base
12531263
return d * self._base
12541264

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

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

12711281
def ge(self, x):
12721282
'return the smallest multiple of base >= x'
1273-
d, m = divmod(x, self._base)
1283+
d, m = _divmod(x, self._base)
12741284
if closeto(m, 0) and not closeto(m / self._base, 1):
12751285
return d * self._base
12761286
return (d + 1) * self._base
@@ -1326,23 +1336,13 @@ def view_limits(self, dmin, dmax):
13261336

13271337

13281338
def scale_range(vmin, vmax, n=1, threshold=100):
1329-
dv = abs(vmax - vmin)
1330-
if dv == 0: # maxabsv == 0 is a special case of this.
1331-
return 1.0, 0.0
1332-
# Note: this should never occur because
1333-
# vmin, vmax should have been checked by nonsingular(),
1334-
# and spread apart if necessary.
1335-
meanv = 0.5 * (vmax + vmin)
1339+
dv = abs(vmax - vmin) # > 0 as nonsingular is called before.
1340+
meanv = (vmax + vmin) / 2
13361341
if abs(meanv) / dv < threshold:
13371342
offset = 0
1338-
elif meanv > 0:
1339-
ex = divmod(math.log10(meanv), 1)[0]
1340-
offset = 10 ** ex
13411343
else:
1342-
ex = divmod(math.log10(-meanv), 1)[0]
1343-
offset = -10 ** ex
1344-
ex = divmod(math.log10(dv / n), 1)[0]
1345-
scale = 10 ** ex
1344+
offset = math.copysign(10 ** (math.log10(abs(meanv)) // 1), meanv)
1345+
scale = 10 ** (math.log10(dv / n) // 1)
13461346
return scale, offset
13471347

13481348

@@ -1352,7 +1352,6 @@ class MaxNLocator(Locator):
13521352
"""
13531353
default_params = dict(nbins=10,
13541354
steps=None,
1355-
trim=True,
13561355
integer=False,
13571356
symmetric=False,
13581357
prune=None)
@@ -1388,9 +1387,6 @@ def __init__(self, *args, **kwargs):
13881387
will be removed. If prune==None, no ticks will be removed.
13891388
13901389
"""
1391-
# I left "trim" out; it defaults to True, and it is not
1392-
# clear that there is any use case for False, so we may
1393-
# want to remove that kwarg. EF 2010/04/18
13941390
if args:
13951391
kwargs['nbins'] = args[0]
13961392
if len(args) > 1:
@@ -1406,7 +1402,9 @@ def set_params(self, **kwargs):
14061402
if self._nbins != 'auto':
14071403
self._nbins = int(self._nbins)
14081404
if 'trim' in kwargs:
1409-
self._trim = kwargs['trim']
1405+
warnings.warn(
1406+
"The 'trim' keyword has no effect since version 2.0.",
1407+
mplDeprecation)
14101408
if 'integer' in kwargs:
14111409
self._integer = kwargs['integer']
14121410
if 'symmetric' in kwargs:
@@ -1429,9 +1427,9 @@ def set_params(self, **kwargs):
14291427
if 'integer' in kwargs:
14301428
self._integer = kwargs['integer']
14311429
if self._integer:
1432-
self._steps = [n for n in self._steps if divmod(n, 1)[1] < 0.001]
1430+
self._steps = [n for n in self._steps if _divmod(n, 1)[1] < 0.001]
14331431

1434-
def bin_boundaries(self, vmin, vmax):
1432+
def _raw_ticks(self, vmin, vmax):
14351433
nbins = self._nbins
14361434
if nbins == 'auto':
14371435
nbins = max(min(self.axis.get_tick_space(), 9), 1)
@@ -1449,23 +1447,30 @@ def bin_boundaries(self, vmin, vmax):
14491447
if step < scaled_raw_step:
14501448
continue
14511449
step *= scale
1452-
best_vmin = step * divmod(vmin, step)[0]
1450+
best_vmin = vmin // step * step
14531451
best_vmax = best_vmin + step * nbins
1454-
if (best_vmax >= vmax):
1452+
if best_vmax >= vmax:
14551453
break
1456-
if self._trim:
1457-
extra_bins = int(divmod((best_vmax - vmax), step)[0])
1458-
nbins -= extra_bins
1459-
return (np.arange(nbins + 1) * step + best_vmin + offset)
1454+
1455+
# More than nbins may be required, e.g. vmin, vmax = -4.1, 4.1 gives
1456+
# nbins=9 but 10 bins are actually required after rounding. So we just
1457+
# create the bins that span the range we need instead.
1458+
low = round(Base(step).le(vmin - best_vmin) / step)
1459+
high = round(Base(step).ge(vmax - best_vmin) / step)
1460+
return np.arange(low, high + 1) * step + best_vmin + offset
1461+
1462+
@cbook.deprecated("2.0")
1463+
def bin_boundaries(self, vmin, vmax):
1464+
return self._raw_ticks(vmin, vmax)
14601465

14611466
def __call__(self):
14621467
vmin, vmax = self.axis.get_view_interval()
14631468
return self.tick_values(vmin, vmax)
14641469

14651470
def tick_values(self, vmin, vmax):
1466-
vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=1e-13,
1467-
tiny=1e-14)
1468-
locs = self.bin_boundaries(vmin, vmax)
1471+
vmin, vmax = mtransforms.nonsingular(
1472+
vmin, vmax, expander=1e-13, tiny=1e-14)
1473+
locs = self._raw_ticks(vmin, vmax)
14691474
prune = self._prune
14701475
if prune == 'lower':
14711476
locs = locs[1:]
@@ -1482,11 +1487,11 @@ def view_limits(self, dmin, dmax):
14821487
dmin = -maxabs
14831488
dmax = maxabs
14841489

1485-
dmin, dmax = mtransforms.nonsingular(dmin, dmax, expander=1e-12,
1486-
tiny=1.e-13)
1490+
dmin, dmax = mtransforms.nonsingular(
1491+
dmin, dmax, expander=1e-12, tiny=1e-13)
14871492

14881493
if rcParams['axes.autolimit_mode'] == 'round_numbers':
1489-
return np.take(self.bin_boundaries(dmin, dmax), [0, -1])
1494+
return self._raw_ticks(dmin, dmax)[[0, -1]]
14901495
else:
14911496
return dmin, dmax
14921497

Binary file not shown.

0 commit comments

Comments
 (0)