Skip to content

Commit 4e1792b

Browse files
committed
Merge pull request #5785 from anntzer/better-offsettext-choice
Better choice of offset-text.
2 parents b16e6f3 + 782b8a1 commit 4e1792b

File tree

3 files changed

+90
-19
lines changed

3 files changed

+90
-19
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Improved offset text choice
2+
---------------------------
3+
The default offset-text choice was changed to only use significant digits that
4+
are common to all ticks (e.g. 1231..1239 -> 1230, instead of 1231), except when
5+
they straddle a relatively large multiple of a power of ten, in which case that
6+
multiple is chosen (e.g. 1999..2001->2000).

lib/matplotlib/tests/test_ticker.py

+48-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from matplotlib.externals import six
55
import nose.tools
6-
from nose.tools import assert_raises
6+
from nose.tools import assert_equal, assert_raises
77
from numpy.testing import assert_almost_equal
88
import numpy as np
99
import matplotlib
@@ -159,6 +159,53 @@ def test_SymmetricalLogLocator_set_params():
159159
nose.tools.assert_equal(sym.numticks, 8)
160160

161161

162+
@cleanup
163+
def test_ScalarFormatter_offset_value():
164+
fig, ax = plt.subplots()
165+
formatter = ax.get_xaxis().get_major_formatter()
166+
167+
def check_offset_for(left, right, offset):
168+
ax.set_xlim(left, right)
169+
# Update ticks.
170+
next(ax.get_xaxis().iter_ticks())
171+
assert_equal(formatter.offset, offset)
172+
173+
test_data = [(123, 189, 0),
174+
(-189, -123, 0),
175+
(12341, 12349, 12340),
176+
(-12349, -12341, -12340),
177+
(99999.5, 100010.5, 100000),
178+
(-100010.5, -99999.5, -100000),
179+
(99990.5, 100000.5, 100000),
180+
(-100000.5, -99990.5, -100000),
181+
(1233999, 1234001, 1234000),
182+
(-1234001, -1233999, -1234000),
183+
(1, 1, 1),
184+
(123, 123, 120),
185+
# Test cases courtesy of @WeatherGod
186+
(.4538, .4578, .45),
187+
(3789.12, 3783.1, 3780),
188+
(45124.3, 45831.75, 45000),
189+
(0.000721, 0.0007243, 0.00072),
190+
(12592.82, 12591.43, 12590),
191+
(9., 12., 0),
192+
(900., 1200., 0),
193+
(1900., 1200., 0),
194+
(0.99, 1.01, 1),
195+
(9.99, 10.01, 10),
196+
(99.99, 100.01, 100),
197+
(5.99, 6.01, 6),
198+
(15.99, 16.01, 16),
199+
(-0.452, 0.492, 0),
200+
(-0.492, 0.492, 0),
201+
(12331.4, 12350.5, 12300),
202+
(-12335.3, 12335.3, 0)]
203+
204+
for left, right, offset in test_data:
205+
yield check_offset_for, left, right, offset
206+
yield check_offset_for, right, left, offset
207+
208+
162209
def _logfe_helper(formatter, base, locs, i, expected_result):
163210
vals = base**locs
164211
labels = [formatter(x, pos) for (x, pos) in zip(vals, i)]

lib/matplotlib/ticker.py

+36-18
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@
164164
from matplotlib.externals import six
165165

166166
import decimal
167+
import itertools
167168
import locale
168169
import math
169170
import numpy as np
@@ -663,33 +664,50 @@ def set_locs(self, locs):
663664
vmin, vmax = self.axis.get_view_interval()
664665
d = abs(vmax - vmin)
665666
if self._useOffset:
666-
self._set_offset(d)
667+
self._compute_offset()
667668
self._set_orderOfMagnitude(d)
668669
self._set_format(vmin, vmax)
669670

670-
def _set_offset(self, range):
671-
# offset of 20,001 is 20,000, for example
671+
def _compute_offset(self):
672672
locs = self.locs
673-
674-
if locs is None or not len(locs) or range == 0:
673+
if locs is None or not len(locs):
675674
self.offset = 0
676675
return
676+
# Restrict to visible ticks.
677677
vmin, vmax = sorted(self.axis.get_view_interval())
678678
locs = np.asarray(locs)
679679
locs = locs[(vmin <= locs) & (locs <= vmax)]
680-
ave_loc = np.mean(locs)
681-
if len(locs) and ave_loc: # dont want to take log10(0)
682-
ave_oom = math.floor(math.log10(np.mean(np.absolute(locs))))
683-
range_oom = math.floor(math.log10(range))
684-
685-
if np.absolute(ave_oom - range_oom) >= 3: # four sig-figs
686-
p10 = 10 ** range_oom
687-
if ave_loc < 0:
688-
self.offset = (math.ceil(np.max(locs) / p10) * p10)
689-
else:
690-
self.offset = (math.floor(np.min(locs) / p10) * p10)
691-
else:
692-
self.offset = 0
680+
if not len(locs):
681+
self.offset = 0
682+
return
683+
lmin, lmax = locs.min(), locs.max()
684+
# Only use offset if there are at least two ticks and every tick has
685+
# the same sign.
686+
if lmin == lmax or lmin <= 0 <= lmax:
687+
self.offset = 0
688+
return
689+
# min, max comparing absolute values (we want division to round towards
690+
# zero so we work on absolute values).
691+
abs_min, abs_max = sorted([abs(float(lmin)), abs(float(lmax))])
692+
sign = math.copysign(1, lmin)
693+
# What is the smallest power of ten such that abs_min and abs_max are
694+
# equal up to that precision?
695+
# Note: Internally using oom instead of 10 ** oom avoids some numerical
696+
# accuracy issues.
697+
oom_max = math.ceil(math.log10(abs_max))
698+
oom = 1 + next(oom for oom in itertools.count(oom_max, -1)
699+
if abs_min // 10 ** oom != abs_max // 10 ** oom)
700+
if (abs_max - abs_min) / 10 ** oom <= 1e-2:
701+
# Handle the case of straddling a multiple of a large power of ten
702+
# (relative to the span).
703+
# What is the smallest power of ten such that abs_min and abs_max
704+
# are no more than 1 apart at that precision?
705+
oom = 1 + next(oom for oom in itertools.count(oom_max, -1)
706+
if abs_max // 10 ** oom - abs_min // 10 ** oom > 1)
707+
# Only use offset if it saves at least two significant digits.
708+
self.offset = (sign * (abs_max // 10 ** oom) * 10 ** oom
709+
if abs_max // 10 ** oom >= 10
710+
else 0)
693711

694712
def _set_orderOfMagnitude(self, range):
695713
# if scientific notation is to be used, find the appropriate exponent

0 commit comments

Comments
 (0)