From 9a4ecfbdf235851a127f22152841631dadbc7558 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Fri, 1 Jan 2016 22:36:52 -0800 Subject: [PATCH 1/5] Better choice of offset-text. The axis offset text is chosen as follows: xlims => offsettext 123, 189 => 0 12341, 12349 => 12340 99999.5, 100010.5 => 100000 # (also a test for #5780) 99990.5, 100000.5 => 100000 1233999, 1234001 => 1234000 (and the same for negative limits). See #5755. --- lib/matplotlib/tests/test_ticker.py | 51 +++++++++++++++++++++++++- lib/matplotlib/ticker.py | 56 +++++++++++++++++++---------- 2 files changed, 88 insertions(+), 19 deletions(-) diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 413286b3b47a..67debcc9fb62 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -3,7 +3,7 @@ from matplotlib.externals import six import nose.tools -from nose.tools import assert_raises +from nose.tools import assert_equal, assert_raises from numpy.testing import assert_almost_equal import numpy as np import matplotlib @@ -159,6 +159,55 @@ def test_SymmetricalLogLocator_set_params(): nose.tools.assert_equal(sym.numticks, 8) +@cleanup +def test_ScalarFormatter_offset_value(): + fig, ax = plt.subplots() + formatter = ax.get_xaxis().get_major_formatter() + + def update_ticks(ax): + return next(ax.get_xaxis().iter_ticks()) + + ax.set_xlim(123, 189) + update_ticks(ax) + assert_equal(formatter.offset, 0) + + ax.set_xlim(-189, -123) + update_ticks(ax) + assert_equal(formatter.offset, 0) + + ax.set_xlim(12341, 12349) + update_ticks(ax) + assert_equal(formatter.offset, 12340) + + ax.set_xlim(-12349, -12341) + update_ticks(ax) + assert_equal(formatter.offset, -12340) + + ax.set_xlim(99999.5, 100010.5) + update_ticks(ax) + assert_equal(formatter.offset, 100000) + + ax.set_xlim(-100010.5, -99999.5) + update_ticks(ax) + assert_equal(formatter.offset, -100000) + + ax.set_xlim(99990.5, 100000.5) + update_ticks(ax) + assert_equal(formatter.offset, 100000) + + ax.set_xlim(-100000.5, -99990.5) + update_ticks(ax) + assert_equal(formatter.offset, -100000) + + ax.set_xlim(1233999, 1234001) + update_ticks(ax) + assert_equal(formatter.offset, 1234000) + + ax.set_xlim(-1234001, -1233999) + update_ticks(ax) + assert_equal(formatter.offset, -1234000) + + def _logfe_helper(formatter, base, locs, i, expected_result): vals = base**locs labels = [formatter(x, pos) for (x, pos) in zip(vals, i)] diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index aec2fee307f9..feb8fa147b8f 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -663,33 +663,53 @@ def set_locs(self, locs): vmin, vmax = self.axis.get_view_interval() d = abs(vmax - vmin) if self._useOffset: - self._set_offset(d) + self._compute_offset() self._set_orderOfMagnitude(d) self._set_format(vmin, vmax) - def _set_offset(self, range): - # offset of 20,001 is 20,000, for example + def _compute_offset(self): locs = self.locs - - if locs is None or not len(locs) or range == 0: + if locs is None or not len(locs): self.offset = 0 return + # Restrict to visible ticks. vmin, vmax = sorted(self.axis.get_view_interval()) locs = np.asarray(locs) locs = locs[(vmin <= locs) & (locs <= vmax)] - ave_loc = np.mean(locs) - if len(locs) and ave_loc: # dont want to take log10(0) - ave_oom = math.floor(math.log10(np.mean(np.absolute(locs)))) - range_oom = math.floor(math.log10(range)) - - if np.absolute(ave_oom - range_oom) >= 3: # four sig-figs - p10 = 10 ** range_oom - if ave_loc < 0: - self.offset = (math.ceil(np.max(locs) / p10) * p10) - else: - self.offset = (math.floor(np.min(locs) / p10) * p10) - else: - self.offset = 0 + if not len(locs): + self.offset = 0 + return + lmin, lmax = locs.min(), locs.max() + # min, max comparing absolute values (we want division to round towards + # zero so we work on absolute values). + abs_min, abs_max = sorted(map(abs, [lmin, lmax])) + # Only use offset if there are at least two ticks, every tick has the + # same sign, and if the span is small compared to the absolute values. + if (lmin == lmax or lmin <= 0 <= lmax or + (abs_max - abs_min) / abs_max >= 1e-2): + self.offset = 0 + return + sign = math.copysign(1, lmin) + # What is the smallest power of ten such that abs_min and abs_max are + # equal up to that precision? + oom = 10 ** int(math.log10(abs_max) + 1) + while True: + if abs_min // oom != abs_max // oom: + oom *= 10 + break + oom /= 10 + if (abs_max - abs_min) / oom <= 1e-2: + # Handle the case of straddling a multiple of a large power of ten + # (relative to the span). + # What is the smallest power of ten such that abs_min and abs_max + # at most 1 apart? + oom = 10 ** int(math.log10(abs_max) + 1) + while True: + if abs_max // oom - abs_min // oom > 1: + oom *= 10 + break + oom /= 10 + self.offset = sign * (abs_max // oom) * oom def _set_orderOfMagnitude(self, range): # if scientific notation is to be used, find the appropriate exponent From f37fd3da92c00d5a7e2fa17ed0bc061adf90670f Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 6 Jan 2016 12:56:38 -0800 Subject: [PATCH 2/5] Only use offset if it saves >=2 sig. digits. Tests cases courtesy of @WeatherGod. Slightly improved numerical accuracy. --- lib/matplotlib/tests/test_ticker.py | 79 ++++++++++++++--------------- lib/matplotlib/ticker.py | 34 +++++++------ 2 files changed, 56 insertions(+), 57 deletions(-) diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 67debcc9fb62..bd49f713a8e7 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -164,48 +164,43 @@ def test_ScalarFormatter_offset_value(): fig, ax = plt.subplots() formatter = ax.get_xaxis().get_major_formatter() - def update_ticks(ax): - return next(ax.get_xaxis().iter_ticks()) - - ax.set_xlim(123, 189) - update_ticks(ax) - assert_equal(formatter.offset, 0) - - ax.set_xlim(-189, -123) - update_ticks(ax) - assert_equal(formatter.offset, 0) - - ax.set_xlim(12341, 12349) - update_ticks(ax) - assert_equal(formatter.offset, 12340) - - ax.set_xlim(-12349, -12341) - update_ticks(ax) - assert_equal(formatter.offset, -12340) - - ax.set_xlim(99999.5, 100010.5) - update_ticks(ax) - assert_equal(formatter.offset, 100000) - - ax.set_xlim(-100010.5, -99999.5) - update_ticks(ax) - assert_equal(formatter.offset, -100000) - - ax.set_xlim(99990.5, 100000.5) - update_ticks(ax) - assert_equal(formatter.offset, 100000) - - ax.set_xlim(-100000.5, -99990.5) - update_ticks(ax) - assert_equal(formatter.offset, -100000) - - ax.set_xlim(1233999, 1234001) - update_ticks(ax) - assert_equal(formatter.offset, 1234000) - - ax.set_xlim(-1234001, -1233999) - update_ticks(ax) - assert_equal(formatter.offset, -1234000) + def check_offset_for(left, right, offset): + ax.set_xlim(left, right) + # Update ticks. + next(ax.get_xaxis().iter_ticks()) + assert_equal(formatter.offset, offset) + + test_data = [(123, 189, 0), + (-189, -123, 0), + (12341, 12349, 12340), + (-12349, -12341, -12340), + (99999.5, 100010.5, 100000), + (-100010.5, -99999.5, -100000), + (99990.5, 100000.5, 100000), + (-100000.5, -99990.5, -100000), + (1233999, 1234001, 1234000), + (-1234001, -1233999, -1234000), + # Test cases courtesy of @WeatherGod + (.4538, .4578, .45), + (3789.12, 3783.1, 3780), + (45124.3, 45831.75, 45000), + (0.000721, 0.0007243, 0.00072), + (12592.82, 12591.43, 12590), + (9., 12., 0), + (900., 1200., 0), + (1900., 1200., 0), + (0.99, 1.01, 1), + (9.99, 10.01, 10), + (99.99, 100.01, 100), + (5.99, 6.01, 6), + (15.99, 16.01, 16), + (-0.452, 0.492, 0), + (-0.492, 0.492, 0), + (12331.4, 12350.5, 12300), + (-12335.3, 12335.3, 0)] + + for left, right, offset in test_data: + yield check_offset_for, left, right, offset def _logfe_helper(formatter, base, locs, i, expected_result): diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index feb8fa147b8f..dc4a5f358201 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -682,34 +682,38 @@ def _compute_offset(self): lmin, lmax = locs.min(), locs.max() # min, max comparing absolute values (we want division to round towards # zero so we work on absolute values). - abs_min, abs_max = sorted(map(abs, [lmin, lmax])) - # Only use offset if there are at least two ticks, every tick has the - # same sign, and if the span is small compared to the absolute values. - if (lmin == lmax or lmin <= 0 <= lmax or - (abs_max - abs_min) / abs_max >= 1e-2): + abs_min, abs_max = sorted([abs(float(lmin)), abs(float(lmax))]) + # Only use offset if there are at least two ticks and every tick has + # the same sign. + if lmin == lmax or lmin <= 0 <= lmax: self.offset = 0 return sign = math.copysign(1, lmin) # What is the smallest power of ten such that abs_min and abs_max are # equal up to that precision? - oom = 10 ** int(math.log10(abs_max) + 1) + # Note: Internally using oom instead of 10 ** oom avoids some numerical + # accuracy issues. + oom = math.ceil(math.log10(abs_max)) while True: - if abs_min // oom != abs_max // oom: - oom *= 10 + if abs_min // 10 ** oom != abs_max // 10 ** oom: + oom += 1 break - oom /= 10 - if (abs_max - abs_min) / oom <= 1e-2: + oom -= 1 + if (abs_max - abs_min) / 10 ** oom <= 1e-2: # Handle the case of straddling a multiple of a large power of ten # (relative to the span). # What is the smallest power of ten such that abs_min and abs_max # at most 1 apart? - oom = 10 ** int(math.log10(abs_max) + 1) + oom = math.ceil(math.log10(abs_max)) while True: - if abs_max // oom - abs_min // oom > 1: - oom *= 10 + if abs_max // 10 ** oom - abs_min // 10 ** oom > 1: + oom += 1 break - oom /= 10 - self.offset = sign * (abs_max // oom) * oom + oom -= 1 + # Only use offset if it saves at least two significant digits. + self.offset = (sign * (abs_max // 10 ** oom) * 10 ** oom + if abs_max // 10 ** oom >= 10 + else 0) def _set_orderOfMagnitude(self, range): # if scientific notation is to be used, find the appropriate exponent From 0c39a78b040e00caf87eee41d448a5e32c26cf54 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 17 Feb 2016 23:52:32 -0800 Subject: [PATCH 3/5] Slightly more efficient impl.; more tests. --- lib/matplotlib/tests/test_ticker.py | 3 +++ lib/matplotlib/ticker.py | 26 ++++++++++---------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index bd49f713a8e7..04f3614f966e 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -180,6 +180,8 @@ def check_offset_for(left, right, offset): (-100000.5, -99990.5, -100000), (1233999, 1234001, 1234000), (-1234001, -1233999, -1234000), + (1, 1, 0), + (123, 123, 123), # Test cases courtesy of @WeatherGod (.4538, .4578, .45), (3789.12, 3783.1, 3780), @@ -201,6 +203,7 @@ def check_offset_for(left, right, offset): for left, right, offset in test_data: yield check_offset_for, left, right, offset + yield check_offset_for, right, left, offset def _logfe_helper(formatter, base, locs, i, expected_result): diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index dc4a5f358201..b0a2672a74b6 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -164,6 +164,7 @@ from matplotlib.externals import six import decimal +import itertools import locale import math import numpy as np @@ -680,36 +681,29 @@ def _compute_offset(self): self.offset = 0 return lmin, lmax = locs.min(), locs.max() - # min, max comparing absolute values (we want division to round towards - # zero so we work on absolute values). - abs_min, abs_max = sorted([abs(float(lmin)), abs(float(lmax))]) # Only use offset if there are at least two ticks and every tick has # the same sign. if lmin == lmax or lmin <= 0 <= lmax: self.offset = 0 return + # min, max comparing absolute values (we want division to round towards + # zero so we work on absolute values). + abs_min, abs_max = sorted([abs(float(lmin)), abs(float(lmax))]) sign = math.copysign(1, lmin) # What is the smallest power of ten such that abs_min and abs_max are # equal up to that precision? # Note: Internally using oom instead of 10 ** oom avoids some numerical # accuracy issues. - oom = math.ceil(math.log10(abs_max)) - while True: - if abs_min // 10 ** oom != abs_max // 10 ** oom: - oom += 1 - break - oom -= 1 + oom_max = math.ceil(math.log10(abs_max)) + oom = 1 + next(oom for oom in itertools.count(oom_max, -1) + if abs_min // 10 ** oom != abs_max // 10 ** oom) if (abs_max - abs_min) / 10 ** oom <= 1e-2: # Handle the case of straddling a multiple of a large power of ten # (relative to the span). # What is the smallest power of ten such that abs_min and abs_max - # at most 1 apart? - oom = math.ceil(math.log10(abs_max)) - while True: - if abs_max // 10 ** oom - abs_min // 10 ** oom > 1: - oom += 1 - break - oom -= 1 + # are no more than 1 apart at that precision? + oom = 1 + next(oom for oom in itertools.count(oom_max, -1) + if abs_max // 10 ** oom - abs_min // 10 ** oom > 1) # Only use offset if it saves at least two significant digits. self.offset = (sign * (abs_max // 10 ** oom) * 10 ** oom if abs_max // 10 ** oom >= 10 From 9d0acb2ba25bc043aab3202c12d52df1fdd213fa Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 1 May 2016 21:12:58 -0700 Subject: [PATCH 4/5] Fix test values. --- lib/matplotlib/tests/test_ticker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 04f3614f966e..94f66e8060e7 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -180,8 +180,8 @@ def check_offset_for(left, right, offset): (-100000.5, -99990.5, -100000), (1233999, 1234001, 1234000), (-1234001, -1233999, -1234000), - (1, 1, 0), - (123, 123, 123), + (1, 1, 1), + (123, 123, 120), # Test cases courtesy of @WeatherGod (.4538, .4578, .45), (3789.12, 3783.1, 3780), From 782b8a13dcce0b97c3ca904a36d5d70284d26fe7 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 2 May 2016 13:13:02 -0700 Subject: [PATCH 5/5] Add What's new entry. --- doc/users/whats_new/offset-text-choice.rst | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 doc/users/whats_new/offset-text-choice.rst diff --git a/doc/users/whats_new/offset-text-choice.rst b/doc/users/whats_new/offset-text-choice.rst new file mode 100644 index 000000000000..4ffdc890c5be --- /dev/null +++ b/doc/users/whats_new/offset-text-choice.rst @@ -0,0 +1,6 @@ +Improved offset text choice +--------------------------- +The default offset-text choice was changed to only use significant digits that +are common to all ticks (e.g. 1231..1239 -> 1230, instead of 1231), except when +they straddle a relatively large multiple of a power of ten, in which case that +multiple is chosen (e.g. 1999..2001->2000).