From 9b98582ee5a2e3cd5a22d1809dacdc5b756ead2a Mon Sep 17 00:00:00 2001 From: Nelle Varoquaux Date: Thu, 10 Nov 2016 10:31:12 -0800 Subject: [PATCH 01/15] ENH ticks sublabel display is now an option closes #7202 --- lib/matplotlib/scale.py | 5 +- lib/matplotlib/tests/test_scale.py | 5 ++ lib/matplotlib/ticker.py | 91 ++++++++++++++++++------------ 3 files changed, 63 insertions(+), 38 deletions(-) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 4ba04e3717dd..d2008ce389be 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -249,7 +249,10 @@ def set_default_locators_and_formatters(self, axis): axis.set_major_locator(LogLocator(self.base)) axis.set_major_formatter(LogFormatterSciNotation(self.base)) axis.set_minor_locator(LogLocator(self.base, self.subs)) - axis.set_minor_formatter(LogFormatterSciNotation(self.base, self.subs)) + axis.set_minor_formatter( + LogFormatterSciNotation(self.base, + labelOnlyBase=self.subs, + sublabel_filtering=True)) def get_transform(self): """ diff --git a/lib/matplotlib/tests/test_scale.py b/lib/matplotlib/tests/test_scale.py index e864372fe346..e5936702773a 100644 --- a/lib/matplotlib/tests/test_scale.py +++ b/lib/matplotlib/tests/test_scale.py @@ -46,3 +46,8 @@ def test_log_scatter(): buf = io.BytesIO() fig.savefig(buf, format='svg') + + +if __name__ == '__main__': + import nose + nose.runmodule(argv=['-s', '--with-doctest'], exit=False) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 0cce991e5a7e..e43d0e0d9544 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -800,14 +800,27 @@ class LogFormatter(Formatter): """ Format values for log axis. """ - def __init__(self, base=10.0, labelOnlyBase=True): + def __init__(self, base=10.0, labelOnlyBase=False, + sublabel_filtering=False): """ `base` is used to locate the decade tick, which will be the only one to be labeled if `labelOnlyBase` is ``True``. + + Parameter + --------- + base : float, optional, default: 10. + base of the logarithm. + + labelOnlyBase : bool, optional, default: False + whether to only label decades ticks. + + sublabel_filtering : bool, optional, default: False + When set to True, label on a subset of ticks. """ self._base = base + 0.0 self.labelOnlyBase = labelOnlyBase - self.sublabel = [1, ] + self.sublabel_filtering = sublabel_filtering + self._sublabels = [1, ] def base(self, base): """ @@ -857,11 +870,11 @@ def set_locs(self, locs): if numdec > 1: # Label only bases - self.sublabel = set((1,)) + self._sublabels = set((1,)) else: # Add labels between bases at log-spaced coefficients c = np.logspace(0, 1, (4 - int(numdec)) + 1, base=b) - self.sublabel = set(np.round(c)) + self._sublabels = set(np.round(c)) def __call__(self, x, pos=None): """ @@ -877,19 +890,20 @@ def __call__(self, x, pos=None): isDecade = is_close_to_int(fx) exponent = np.round(fx) if isDecade else np.floor(fx) coeff = np.round(x / b ** exponent) - if coeff in self.sublabel: - if not isDecade and self.labelOnlyBase: - return '' - elif x > 10000: - s = '%1.0e' % x - elif x < 1: - s = '%1.0e' % x - else: - s = self.pprint_val(x, self.d) - if sign == -1: - s = '-%s' % s + + if self.sublabel_filtering and coeff not in self._sublabels: + return '' + if not isDecade and self.labelOnlyBase: + return '' + + if x > 10000: + s = '%1.0e' % x + elif x < 1: + s = '%1.0e' % x else: - s = '' + s = self.pprint_val(x, self.d) + if sign == -1: + s = '-%s' % s return self.fix_minus(s) @@ -958,10 +972,17 @@ def __call__(self, x, pos=None): sign = np.sign(x) # only label the decades fx = math.log(abs(x)) / math.log(b) + isDecade = is_close_to_int(fx) + exponent = np.round(fx) if is_decade else np.floor(fx) + coeff = np.round(abs(x) / b ** exponent) + + if self.sublabel_filtering and coeff not in self._sublabels: + return '' if not isDecade and self.labelOnlyBase: - s = '' - elif abs(fx) > 10000: + return '' + + if abs(fx) > 10000: s = '%1.0g' % fx elif abs(fx) < 1: s = '%1.0g' % fx @@ -1016,22 +1037,22 @@ def __call__(self, x, pos=None): else: base = '%s' % b - if coeff in self.sublabel: - if not is_decade and self.labelOnlyBase: - return '' - elif not is_decade: - return self._non_decade_format(sign_string, base, fx, usetex) - else: - if usetex: - return (r'$%s%s^{%d}$') % (sign_string, - base, - nearest_long(fx)) - else: - return ('$%s$' % _mathdefault( - '%s%s^{%d}' % - (sign_string, base, nearest_long(fx)))) - else: + if self.sublabel_filtering and coeff not in self._sublabels: return '' + if not is_decade and self.labelOnlyBase: + return '' + + if not is_decade: + return self._non_decade_format(sign_string, base, fx, usetex) + else: + if usetex: + return (r'$%s%s^{%d}$') % (sign_string, + base, + nearest_long(fx)) + else: + return ('$%s$' % _mathdefault( + '%s%s^{%d}' % + (sign_string, base, nearest_long(fx)))) class LogFormatterSciNotation(LogFormatterMathtext): @@ -1039,10 +1060,6 @@ class LogFormatterSciNotation(LogFormatterMathtext): Format values following scientific notation in a logarithmic axis """ - def __init__(self, base=10.0, labelOnlyBase=False): - super(LogFormatterSciNotation, self).__init__(base=base, - labelOnlyBase=labelOnlyBase) - def _non_decade_format(self, sign_string, base, fx, usetex): 'Return string for non-decade locations' b = float(base) From e6512b239f40c00cddabc646fc9b11b88af87b19 Mon Sep 17 00:00:00 2001 From: Nelle Varoquaux Date: Thu, 10 Nov 2016 10:55:42 -0800 Subject: [PATCH 02/15] FIX updated ticker tests --- lib/matplotlib/tests/test_ticker.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index a600335a73b3..7470997f0821 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -240,8 +240,10 @@ def test_LogFormatter_sublabel(): ax.xaxis.set_major_locator(mticker.LogLocator(base=10, subs=[])) ax.xaxis.set_minor_locator(mticker.LogLocator(base=10, subs=np.arange(2, 10))) - ax.xaxis.set_major_formatter(mticker.LogFormatter()) - ax.xaxis.set_minor_formatter(mticker.LogFormatter(labelOnlyBase=False)) + ax.xaxis.set_major_formatter(mticker.LogFormatter(labelOnlyBase=True)) + ax.xaxis.set_minor_formatter(mticker.LogFormatter( + labelOnlyBase=False, + sublabel_filtering=True)) # axis range above 3 decades, only bases are labeled ax.set_xlim(1, 1e4) fmt = ax.xaxis.get_major_formatter() From 1610ce5985dc1ebe3b6ba182e060035f8623d5d3 Mon Sep 17 00:00:00 2001 From: Nelle Varoquaux Date: Thu, 10 Nov 2016 11:32:49 -0800 Subject: [PATCH 03/15] FIX LogFormatterSciNotation now works fine with negative non decade values --- lib/matplotlib/ticker.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index e43d0e0d9544..f3ec8b316553 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -836,7 +836,11 @@ def label_minor(self, labelOnlyBase): """ Switch minor tick labeling on or off. - ``labelOnlyBase=True`` to turn off minor ticks. + Parameters + ---------- + labelOnlyBase : bool, optional, default: True + If True, only label decades. + """ self.labelOnlyBase = labelOnlyBase @@ -878,7 +882,7 @@ def set_locs(self, locs): def __call__(self, x, pos=None): """ - Return the format for tick val `x` at position `pos`. + Return the format for tick val `x`. """ b = self._base if x == 0.0: @@ -960,8 +964,6 @@ class LogFormatterExponent(LogFormatter): def __call__(self, x, pos=None): """ Return the format for tick value `x`. - - The position `pos` is ignored. """ vmin, vmax = self.axis.get_view_interval() vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=0.05) @@ -1068,11 +1070,11 @@ def _non_decade_format(self, sign_string, base, fx, usetex): if is_close_to_int(coeff): coeff = nearest_long(coeff) if usetex: - return (r'$%g\times%s^{%d}$') % \ - (coeff, base, exponent) + return (r'$%s%g\times%s^{%d}$') % \ + (sign_string, coeff, base, exponent) else: - return ('$%s$' % _mathdefault(r'%g\times%s^{%d}' % - (coeff, base, exponent))) + return ('$%s$' % _mathdefault(r'%s%g\times%s^{%d}' % + (sign_string, coeff, base, exponent))) class LogitFormatter(Formatter): From 8f53696ff797d7d4e69fa520e57b3cbd3fe6cefc Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 11 Nov 2016 14:26:37 -0500 Subject: [PATCH 04/15] FIX: remove non-breaking space --- lib/matplotlib/ticker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index f3ec8b316553..8c341d0acea8 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -808,7 +808,7 @@ def __init__(self, base=10.0, labelOnlyBase=False, Parameter --------- - base : float, optional, default: 10. + base : float, optional, default: 10. base of the logarithm. labelOnlyBase : bool, optional, default: False From 80dc75ac3818def375acb2edc55723374826ac82 Mon Sep 17 00:00:00 2001 From: Nelle Varoquaux Date: Fri, 11 Nov 2016 14:23:25 -0800 Subject: [PATCH 05/15] MAINT renamed sublabel_filtering to label_pruning --- lib/matplotlib/scale.py | 2 +- lib/matplotlib/tests/test_ticker.py | 2 +- lib/matplotlib/ticker.py | 22 +++++++++++----------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index d2008ce389be..b90701fa49d7 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -252,7 +252,7 @@ def set_default_locators_and_formatters(self, axis): axis.set_minor_formatter( LogFormatterSciNotation(self.base, labelOnlyBase=self.subs, - sublabel_filtering=True)) + label_pruning=True)) def get_transform(self): """ diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 7470997f0821..805fd0cec262 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -243,7 +243,7 @@ def test_LogFormatter_sublabel(): ax.xaxis.set_major_formatter(mticker.LogFormatter(labelOnlyBase=True)) ax.xaxis.set_minor_formatter(mticker.LogFormatter( labelOnlyBase=False, - sublabel_filtering=True)) + label_pruning=True)) # axis range above 3 decades, only bases are labeled ax.set_xlim(1, 1e4) fmt = ax.xaxis.get_major_formatter() diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 8c341d0acea8..3d5dd617c9be 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -801,25 +801,25 @@ class LogFormatter(Formatter): Format values for log axis. """ def __init__(self, base=10.0, labelOnlyBase=False, - sublabel_filtering=False): + label_pruning=False): """ `base` is used to locate the decade tick, which will be the only one to be labeled if `labelOnlyBase` is ``True``. - Parameter - --------- + Parameters + ---------- base : float, optional, default: 10. base of the logarithm. labelOnlyBase : bool, optional, default: False - whether to only label decades ticks. + whether to only label decades ticks. - sublabel_filtering : bool, optional, default: False - When set to True, label on a subset of ticks. + label_pruning : bool, optional, default: False + when set to True, label on a subset of ticks. """ self._base = base + 0.0 self.labelOnlyBase = labelOnlyBase - self.sublabel_filtering = sublabel_filtering + self.label_pruning = label_pruning self._sublabels = [1, ] def base(self, base): @@ -895,7 +895,7 @@ def __call__(self, x, pos=None): exponent = np.round(fx) if isDecade else np.floor(fx) coeff = np.round(x / b ** exponent) - if self.sublabel_filtering and coeff not in self._sublabels: + if self.label_pruning and coeff not in self._sublabels: return '' if not isDecade and self.labelOnlyBase: return '' @@ -976,10 +976,10 @@ def __call__(self, x, pos=None): fx = math.log(abs(x)) / math.log(b) isDecade = is_close_to_int(fx) - exponent = np.round(fx) if is_decade else np.floor(fx) + exponent = np.round(fx) if isDecade else np.floor(fx) coeff = np.round(abs(x) / b ** exponent) - if self.sublabel_filtering and coeff not in self._sublabels: + if self.label_pruning and coeff not in self._sublabels: return '' if not isDecade and self.labelOnlyBase: return '' @@ -1039,7 +1039,7 @@ def __call__(self, x, pos=None): else: base = '%s' % b - if self.sublabel_filtering and coeff not in self._sublabels: + if self.label_pruning and coeff not in self._sublabels: return '' if not is_decade and self.labelOnlyBase: return '' From 46e237a85b016ef4e0f1c6b18d358e580837b51e Mon Sep 17 00:00:00 2001 From: Nelle Varoquaux Date: Mon, 14 Nov 2016 13:47:38 -0800 Subject: [PATCH 06/15] FIX misalignment --- lib/matplotlib/ticker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 3d5dd617c9be..837d6585d327 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -1050,7 +1050,7 @@ def __call__(self, x, pos=None): if usetex: return (r'$%s%s^{%d}$') % (sign_string, base, - nearest_long(fx)) + nearest_long(fx)) else: return ('$%s$' % _mathdefault( '%s%s^{%d}' % From f718a3b7387980eea020c07265ad83f3398f21e7 Mon Sep 17 00:00:00 2001 From: Eric Firing Date: Tue, 15 Nov 2016 12:28:46 -1000 Subject: [PATCH 07/15] Improve the minor tick labeling by LogFormatter. --- lib/matplotlib/scale.py | 3 +- lib/matplotlib/tests/test_ticker.py | 12 ++++---- lib/matplotlib/ticker.py | 43 +++++++++++++++++------------ 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index b90701fa49d7..7f7a0865654c 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -251,8 +251,7 @@ def set_default_locators_and_formatters(self, axis): axis.set_minor_locator(LogLocator(self.base, self.subs)) axis.set_minor_formatter( LogFormatterSciNotation(self.base, - labelOnlyBase=self.subs, - label_pruning=True)) + labelOnlyBase=self.subs)) def get_transform(self): """ diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 805fd0cec262..7c170d76252c 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -241,9 +241,7 @@ def test_LogFormatter_sublabel(): ax.xaxis.set_minor_locator(mticker.LogLocator(base=10, subs=np.arange(2, 10))) ax.xaxis.set_major_formatter(mticker.LogFormatter(labelOnlyBase=True)) - ax.xaxis.set_minor_formatter(mticker.LogFormatter( - labelOnlyBase=False, - label_pruning=True)) + ax.xaxis.set_minor_formatter(mticker.LogFormatter(labelOnlyBase=False)) # axis range above 3 decades, only bases are labeled ax.set_xlim(1, 1e4) fmt = ax.xaxis.get_major_formatter() @@ -263,9 +261,13 @@ def test_LogFormatter_sublabel(): ax.set_xlim(1, 80) _sub_labels(ax.xaxis, subs=[]) - # axis range at 0 to 1 decades, label subs 2, 3, 6 + # axis range at 0.4 to 1 decades, label subs 2, 3, 4, 6 ax.set_xlim(1, 8) - _sub_labels(ax.xaxis, subs=[2, 3, 6]) + _sub_labels(ax.xaxis, subs=[2, 3, 4, 6]) + + # axis range at 0 to 0.4 decades, label all + ax.set_xlim(0.5, 0.9) + _sub_labels(ax.xaxis, subs=np.arange(2, 10, dtype=int)) def _logfe_helper(formatter, base, locs, i, expected_result): diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 837d6585d327..43fdb62d3c56 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -801,7 +801,7 @@ class LogFormatter(Formatter): Format values for log axis. """ def __init__(self, base=10.0, labelOnlyBase=False, - label_pruning=False): + minor_thresholds=(1, 0.4)): """ `base` is used to locate the decade tick, which will be the only one to be labeled if `labelOnlyBase` is ``True``. @@ -812,15 +812,21 @@ def __init__(self, base=10.0, labelOnlyBase=False, base of the logarithm. labelOnlyBase : bool, optional, default: False - whether to only label decades ticks. + whether label ticks only at integer multiples of base. + This is normally True for major ticks and False for + minor ticks. - label_pruning : bool, optional, default: False - when set to True, label on a subset of ticks. + minor_thresholds : (subset, all), optional, default: (1, 0.4) + Thresholds applied to the data range measured in powers + of the base (numbers of "decades", or 'numdec'), and + effective only when labelOnlyBase is False. Then a + subset of minor ticks will be labeled if `numdec <= subset`, + and all will be labeled if `numdec <= all`. """ self._base = base + 0.0 self.labelOnlyBase = labelOnlyBase - self.label_pruning = label_pruning - self._sublabels = [1, ] + self.minor_thresholds = minor_thresholds + self._sublabels = None def base(self, base): """ @@ -872,13 +878,15 @@ def set_locs(self, locs): vmax = math.log(vmax) / math.log(b) numdec = abs(vmax - vmin) - if numdec > 1: + if numdec > self.minor_thresholds[0]: # Label only bases self._sublabels = set((1,)) - else: + elif numdec > self.minor_thresholds[1]: # Add labels between bases at log-spaced coefficients - c = np.logspace(0, 1, (4 - int(numdec)) + 1, base=b) + c = np.logspace(0, 1, b//2 + 1, base=b)[1:-1] self._sublabels = set(np.round(c)) + else: + self._sublabels = set(np.linspace(2, b-1, b-2)) def __call__(self, x, pos=None): """ @@ -895,9 +903,9 @@ def __call__(self, x, pos=None): exponent = np.round(fx) if isDecade else np.floor(fx) coeff = np.round(x / b ** exponent) - if self.label_pruning and coeff not in self._sublabels: + if self.labelOnlyBase and not isDecade: return '' - if not isDecade and self.labelOnlyBase: + if self._sublabels is not None and coeff not in self._sublabels: return '' if x > 10000: @@ -972,16 +980,17 @@ def __call__(self, x, pos=None): if x == 0: return '0' sign = np.sign(x) + x = abs(x) # only label the decades - fx = math.log(abs(x)) / math.log(b) + fx = math.log(x) / math.log(b) isDecade = is_close_to_int(fx) exponent = np.round(fx) if isDecade else np.floor(fx) - coeff = np.round(abs(x) / b ** exponent) + coeff = np.round(x / b ** exponent) - if self.label_pruning and coeff not in self._sublabels: + if self.labelOnlyBase and not isDecade: return '' - if not isDecade and self.labelOnlyBase: + if self._sublabels is not None and coeff not in self._sublabels: return '' if abs(fx) > 10000: @@ -1039,9 +1048,9 @@ def __call__(self, x, pos=None): else: base = '%s' % b - if self.label_pruning and coeff not in self._sublabels: + if self.labelOnlyBase and not isDecade: return '' - if not is_decade and self.labelOnlyBase: + if self._sublabels is not None and coeff not in self._sublabels: return '' if not is_decade: From 0f2dfa89b6cf7c583b6b1028ca2cfba12b6d57e7 Mon Sep 17 00:00:00 2001 From: Nelle Varoquaux Date: Wed, 16 Nov 2016 14:43:49 -0800 Subject: [PATCH 08/15] FIX variable isDecade -> is_decode --- lib/matplotlib/ticker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 43fdb62d3c56..e0cc2f8f5dda 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -1048,7 +1048,7 @@ def __call__(self, x, pos=None): else: base = '%s' % b - if self.labelOnlyBase and not isDecade: + if self.labelOnlyBase and not is_decade: return '' if self._sublabels is not None and coeff not in self._sublabels: return '' From 5608c955121dbe334af8b307006f4c49630e36f6 Mon Sep 17 00:00:00 2001 From: Nelle Varoquaux Date: Wed, 16 Nov 2016 14:54:52 -0800 Subject: [PATCH 09/15] MAINT renamed isDecade and is_decade in is_x_decade --- lib/matplotlib/ticker.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index e0cc2f8f5dda..f6bf22119be9 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -899,11 +899,11 @@ def __call__(self, x, pos=None): x = abs(x) # only label the decades fx = math.log(x) / math.log(b) - isDecade = is_close_to_int(fx) - exponent = np.round(fx) if isDecade else np.floor(fx) + is_x_decade = is_close_to_int(fx) + exponent = np.round(fx) if is_x_decade else np.floor(fx) coeff = np.round(x / b ** exponent) - if self.labelOnlyBase and not isDecade: + if self.labelOnlyBase and not is_x_decade: return '' if self._sublabels is not None and coeff not in self._sublabels: return '' @@ -984,11 +984,11 @@ def __call__(self, x, pos=None): # only label the decades fx = math.log(x) / math.log(b) - isDecade = is_close_to_int(fx) - exponent = np.round(fx) if isDecade else np.floor(fx) + is_x_decade = is_close_to_int(fx) + exponent = np.round(fx) if is_x_decade else np.floor(fx) coeff = np.round(x / b ** exponent) - if self.labelOnlyBase and not isDecade: + if self.labelOnlyBase and not is_x_decade: return '' if self._sublabels is not None and coeff not in self._sublabels: return '' @@ -1036,8 +1036,8 @@ def __call__(self, x, pos=None): return '$%s$' % _mathdefault('0') fx = math.log(abs(x)) / math.log(b) - is_decade = is_close_to_int(fx) - exponent = np.round(fx) if is_decade else np.floor(fx) + is_x_decade = is_close_to_int(fx) + exponent = np.round(fx) if is_x_decade else np.floor(fx) coeff = np.round(abs(x) / b ** exponent) sign_string = '-' if x < 0 else '' @@ -1048,12 +1048,12 @@ def __call__(self, x, pos=None): else: base = '%s' % b - if self.labelOnlyBase and not is_decade: + if self.labelOnlyBase and not is_x_decade: return '' if self._sublabels is not None and coeff not in self._sublabels: return '' - if not is_decade: + if not is_x_decade: return self._non_decade_format(sign_string, base, fx, usetex) else: if usetex: From 1fe5820b960b946779f6d3d47dcc312d4f7efea0 Mon Sep 17 00:00:00 2001 From: Eric Firing Date: Tue, 22 Nov 2016 12:30:01 -1000 Subject: [PATCH 10/15] Improved LogNorm ticks and tick labels on colorbar --- lib/matplotlib/colorbar.py | 11 ++- lib/matplotlib/ticker.py | 136 ++++++++++++++++++++++++++----------- 2 files changed, 106 insertions(+), 41 deletions(-) diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index a7c6c97249fe..621126f40481 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -310,8 +310,8 @@ def __init__(self, ax, cmap=None, else: self.locator = ticks # Handle default in _ticker() if format is None: - if isinstance(self.norm, colors.LogNorm): - self.formatter = ticker.LogFormatterMathtext() + if isinstance(self.norm, (colors.LogNorm, colors.SymLogNorm)): + self.formatter = ticker.LogFormatterSciNotation() else: self.formatter = ticker.ScalarFormatter() elif cbook.is_string_like(format): @@ -574,7 +574,12 @@ def _ticker(self): b = self.norm.boundaries locator = ticker.FixedLocator(b, nbins=10) elif isinstance(self.norm, colors.LogNorm): - locator = ticker.LogLocator() + locator = ticker.LogLocator(subs='all') + # FIXME: we should be able to use the SymmetricalLogLocator + # but we need to give it a transform, or modify the + # locator to get what it needs from the Norm. + # elif isinstance(self.norm, colors.SymLogNorm): + # locator = ticker.SymmetricalLogLocator(...) else: if mpl.rcParams['_internal.classic_mode']: locator = ticker.MaxNLocator() diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index f6bf22119be9..721b24676677 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -798,32 +798,58 @@ def _formatSciNotation(self, s): class LogFormatter(Formatter): """ - Format values for log axis. + Base class for formatting ticks on a log or symlog scale. + + It may be instantiated directly, or subclassed. + + Parameters + ---------- + base : float, optional, default: 10. + Base of the logarithm used in all calculations. + + labelOnlyBase : bool, optional, default: False + If True, label ticks only at integer powers of base. + This is normally True for major ticks and False for + minor ticks. + + minor_thresholds : (subset, all), optional, default: (1, 0.4) + If labelOnlyBase is False, these two numbers control + the labeling of ticks that are not at integer powers of + base; normally these are the minor ticks. The controlling + parameter is the log of the axis data range. In the typical + case where base is 10 it is the number of decades spanned + by the axis, so we can call it 'numdec'. If ``numdec <= all``, + all minor ticks will be labeled. If ``all < numdec <= subset``, + then only a subset of minor ticks will be labeled, so as to + avoid crowding. If ``numdec > subset`` then no minor ticks will + be labeled. + + Notes + ----- + The `set_locs` method must be called to enable the subsetting + logic controlled by the ``minor_thresholds`` parameter. + + In some cases such as the colorbar, there is no distinction between + major and minor ticks; the tick locations might be set manually, + or by a locator that puts ticks at integer powers of base and + at intermediate locations. For this situation, disable the + minor_thresholds logic by using ``minor_thresholds=(np.inf, np.inf)``. + + Examples + -------- + To label a subset of minor ticks when the view limits span up + to 2 decades, and all of the ticks when zoomed in to 0.5 decades + or less, use ``minor_thresholds=(2, 0.5)``. + + To label all minor ticks when the view limits span up to 1.5 + decades, use ``minor_thresholds=(1.5, 1.5)``. + + """ def __init__(self, base=10.0, labelOnlyBase=False, minor_thresholds=(1, 0.4)): - """ - `base` is used to locate the decade tick, which will be the only - one to be labeled if `labelOnlyBase` is ``True``. - Parameters - ---------- - base : float, optional, default: 10. - base of the logarithm. - - labelOnlyBase : bool, optional, default: False - whether label ticks only at integer multiples of base. - This is normally True for major ticks and False for - minor ticks. - - minor_thresholds : (subset, all), optional, default: (1, 0.4) - Thresholds applied to the data range measured in powers - of the base (numbers of "decades", or 'numdec'), and - effective only when labelOnlyBase is False. Then a - subset of minor ticks will be labeled if `numdec <= subset`, - and all will be labeled if `numdec <= all`. - """ - self._base = base + 0.0 + self._base = float(base) self.labelOnlyBase = labelOnlyBase self.minor_thresholds = minor_thresholds self._sublabels = None @@ -850,17 +876,24 @@ def label_minor(self, labelOnlyBase): """ self.labelOnlyBase = labelOnlyBase - def set_locs(self, locs): + def set_locs(self, locs=None): + """ + Use axis view limits to control which ticks are labeled. + + The ``locs`` parameter is ignored in the present algorithm. + + """ + if np.isinf(self.minor_thresholds[0]): + self._sublabels = None + return + b = self._base vmin, vmax = self.axis.get_view_interval() self.d = abs(vmax - vmin) - if not hasattr(self.axis, 'get_transform'): - # This might be a colorbar dummy axis, do not attempt to get - # transform - numdec = 10 - elif hasattr(self.axis.get_transform(), 'linthresh'): + if (hasattr(self.axis, 'get_transform') and + hasattr(self.axis.get_transform(), 'linthresh')): t = self.axis.get_transform() linthresh = t.linthresh # Only compute the number of decades in the logarithmic part of the @@ -882,11 +915,13 @@ def set_locs(self, locs): # Label only bases self._sublabels = set((1,)) elif numdec > self.minor_thresholds[1]: - # Add labels between bases at log-spaced coefficients - c = np.logspace(0, 1, b//2 + 1, base=b)[1:-1] + # Add labels between bases at log-spaced coefficients; + # include base powers in case the locations include + # "major" and "minor" points, as in colorbar. + c = np.logspace(0, 1, b//2 + 1, base=b) self._sublabels = set(np.round(c)) else: - self._sublabels = set(np.linspace(2, b-1, b-2)) + self._sublabels = set(np.linspace(1, b, b-2)) def __call__(self, x, pos=None): """ @@ -1809,7 +1844,22 @@ class LogLocator(Locator): def __init__(self, base=10.0, subs=(1.0,), numdecs=4, numticks=None): """ - place ticks on the location= base**i*subs[j] + Place ticks on the locations : subs[j] * base**i + + Parameters + ---------- + subs : None, string, or sequence of float, optional, default (1.0,) + Gives the multiples of integer powers of the base at which + to place ticks. The default places ticks only at + integer powers of the base. + The permitted string values are ``'auto'`` and ``'all'``, + both of which use an algorithm based on the axis view + limits to determine whether and how to put ticks between + integer powers of the base. With ``'auto'``, ticks are + placed only between integer powers; with ``'all'``, the + integer powers are included. A value of None is + equivalent to ``'auto'``. + """ if numticks is None: if rcParams['_internal.classic_mode']: @@ -1832,6 +1882,9 @@ def set_params(self, base=None, subs=None, numdecs=None, numticks=None): if numticks is not None: self.numticks = numticks + # FIXME: these base and subs functions are contrary to our + # usual and desired API. + def base(self, base): """ set the base of the log scaling (major tick every base**i, i integer) @@ -1842,8 +1895,11 @@ def subs(self, subs): """ set the minor ticks for the log scaling every base**i*subs[j] """ - if subs is None: - self._subs = None # autosub + if subs is None: # consistency with previous bad API + self._subs = 'auto' + elif cbook.is_string_like(subs): + # TODO: validation ('all', 'auto') + self._subs = subs else: self._subs = np.asarray(subs, dtype=float) @@ -1887,13 +1943,17 @@ def tick_values(self, vmin, vmax): numdec = math.floor(vmax) - math.ceil(vmin) - if self._subs is None: # autosub for minor ticks + if cbook.is_string_like(self._subs): + _first = 2.0 if self._subs == 'auto' else 1.0 if numdec > 10 or b < 3: - return np.array([]) # no minor ticks + if self._subs == 'auto': + return np.array([]) # no minor or major ticks + else: + subs = np.array([1.0]) # major ticks elif numdec > 5 and b >= 6: - subs = np.arange(2.0, b, 2.0) + subs = np.arange(_first, b, 2.0) else: - subs = np.arange(2.0, b) + subs = np.arange(_first, b) else: subs = self._subs From 26d5be6fc45fc88445094165e0e2b1dacd6248f4 Mon Sep 17 00:00:00 2001 From: Eric Firing Date: Tue, 22 Nov 2016 14:03:37 -1000 Subject: [PATCH 11/15] Improve automatic ticks and labels for colorbar with SymLogNorm --- lib/matplotlib/colorbar.py | 17 +++++++---- lib/matplotlib/ticker.py | 59 +++++++++++++++++++++++++------------- 2 files changed, 50 insertions(+), 26 deletions(-) diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 621126f40481..be3db57e974f 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -310,8 +310,11 @@ def __init__(self, ax, cmap=None, else: self.locator = ticks # Handle default in _ticker() if format is None: - if isinstance(self.norm, (colors.LogNorm, colors.SymLogNorm)): + if isinstance(self.norm, colors.LogNorm): self.formatter = ticker.LogFormatterSciNotation() + elif isinstance(self.norm, colors.SymLogNorm): + self.formatter = ticker.LogFormatterSciNotation( + linthresh=self.norm.linthresh) else: self.formatter = ticker.ScalarFormatter() elif cbook.is_string_like(format): @@ -575,11 +578,13 @@ def _ticker(self): locator = ticker.FixedLocator(b, nbins=10) elif isinstance(self.norm, colors.LogNorm): locator = ticker.LogLocator(subs='all') - # FIXME: we should be able to use the SymmetricalLogLocator - # but we need to give it a transform, or modify the - # locator to get what it needs from the Norm. - # elif isinstance(self.norm, colors.SymLogNorm): - # locator = ticker.SymmetricalLogLocator(...) + elif isinstance(self.norm, colors.SymLogNorm): + # The subs setting here should be replaced + # by logic in the locator. + locator = ticker.SymmetricalLogLocator( + subs=np.arange(1, 10), + linthresh=self.norm.linthresh, + base=10) else: if mpl.rcParams['_internal.classic_mode']: locator = ticker.MaxNLocator() diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 721b24676677..48c2f5d5bbaa 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -824,6 +824,10 @@ class LogFormatter(Formatter): avoid crowding. If ``numdec > subset`` then no minor ticks will be labeled. + linthresh: None or float, optional, default: None + If a symmetric log scale is in use, its ``linthresh`` + parameter must be supplied here. + Notes ----- The `set_locs` method must be called to enable the subsetting @@ -847,12 +851,14 @@ class LogFormatter(Formatter): """ def __init__(self, base=10.0, labelOnlyBase=False, - minor_thresholds=(1, 0.4)): + minor_thresholds=(1, 0.4), + linthresh=None): self._base = float(base) self.labelOnlyBase = labelOnlyBase self.minor_thresholds = minor_thresholds self._sublabels = None + self._linthresh = linthresh def base(self, base): """ @@ -892,20 +898,24 @@ def set_locs(self, locs=None): vmin, vmax = self.axis.get_view_interval() self.d = abs(vmax - vmin) - if (hasattr(self.axis, 'get_transform') and - hasattr(self.axis.get_transform(), 'linthresh')): - t = self.axis.get_transform() - linthresh = t.linthresh + # Handle symlog case: + linthresh = self._linthresh + if linthresh is None: + try: + linthresh = self.axis.get_transform().linthresh + except AttributeError: + pass + + if linthresh is not None: # symlog # Only compute the number of decades in the logarithmic part of the # axis numdec = 0 if vmin < -linthresh: - numdec += math.log(-vmin / linthresh) / math.log(b) - - if vmax > linthresh and vmin < linthresh: - numdec += math.log(vmax / linthresh) / math.log(b) - elif vmin >= linthresh: - numdec += math.log(vmax / vmin) / math.log(b) + rhs = min(vmax, -linthresh) + numdec += math.log(vmin / rhs) / math.log(b) + if vmax > linthresh: + lhs = max(vmin, linthresh) + numdec += math.log(vmax / lhs) / math.log(b) else: vmin = math.log(vmin) / math.log(b) vmax = math.log(vmax) / math.log(b) @@ -1070,12 +1080,13 @@ def __call__(self, x, pos=None): else: return '$%s$' % _mathdefault('0') - fx = math.log(abs(x)) / math.log(b) + sign_string = '-' if x < 0 else '' + x = abs(x) + + fx = math.log(x) / math.log(b) is_x_decade = is_close_to_int(fx) exponent = np.round(fx) if is_x_decade else np.floor(fx) - coeff = np.round(abs(x) / b ** exponent) - - sign_string = '-' if x < 0 else '' + coeff = np.round(x / b ** exponent) # use string formatting of the base if it is not an integer if b % 1 == 0.0: @@ -2021,14 +2032,22 @@ def view_limits(self, vmin, vmax): class SymmetricalLogLocator(Locator): """ - Determine the tick locations for log axes + Determine the tick locations for symmetric log axes """ - def __init__(self, transform, subs=None): + def __init__(self, transform=None, subs=None, linthresh=None, base=None): """ place ticks on the location= base**i*subs[j] """ - self._transform = transform + if transform is not None: + self._base = transform.base + self._linthresh = transform.linthresh + elif linthresh is not None and base is not None: + self._base = base + self._linthresh = linthresh + else: + raise ValueError("Either transform or linthresh " + "and base must be provided.") if subs is None: self._subs = [1.0] else: @@ -2049,8 +2068,8 @@ def __call__(self): return self.tick_values(vmin, vmax) def tick_values(self, vmin, vmax): - b = self._transform.base - t = self._transform.linthresh + b = self._base + t = self._linthresh if vmax < vmin: vmin, vmax = vmax, vmin From 5824343d1056792538e7d45a63e97747355b3b68 Mon Sep 17 00:00:00 2001 From: Nelle Varoquaux Date: Tue, 22 Nov 2016 18:06:38 -0800 Subject: [PATCH 12/15] FIX numpydoc now renders LogFormatterSciNotation properly --- lib/matplotlib/ticker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 48c2f5d5bbaa..0d43e675fc5e 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -824,7 +824,7 @@ class LogFormatter(Formatter): avoid crowding. If ``numdec > subset`` then no minor ticks will be labeled. - linthresh: None or float, optional, default: None + linthresh : None or float, optional, default: None If a symmetric log scale is in use, its ``linthresh`` parameter must be supplied here. From ee0e513279821007a75f7868ecc0fc3cb72bb049 Mon Sep 17 00:00:00 2001 From: Eric Firing Date: Tue, 22 Nov 2016 20:36:34 -1000 Subject: [PATCH 13/15] fix remaining bugs that broke the tests --- lib/matplotlib/tests/test_ticker.py | 3 +-- lib/matplotlib/ticker.py | 7 +++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 7c170d76252c..e60ba93276ac 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -162,8 +162,7 @@ def test_SymmetricalLogLocator_set_params(): See if change was successful. Should not exception. """ - # since we only test for the params change. I will pass empty transform - sym = mticker.SymmetricalLogLocator(None) + sym = mticker.SymmetricalLogLocator(base=10, linthresh=1) sym.set_params(subs=[2.0], numticks=8) nose.tools.assert_equal(sym._subs, [2.0]) nose.tools.assert_equal(sym.numticks, 8) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 0d43e675fc5e..a91f66d80d66 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -906,7 +906,7 @@ def set_locs(self, locs=None): except AttributeError: pass - if linthresh is not None: # symlog + if linthresh is not None: # symlog # Only compute the number of decades in the logarithmic part of the # axis numdec = 0 @@ -931,7 +931,7 @@ def set_locs(self, locs=None): c = np.logspace(0, 1, b//2 + 1, base=b) self._sublabels = set(np.round(c)) else: - self._sublabels = set(np.linspace(1, b, b-2)) + self._sublabels = set(np.linspace(1, b, b)) def __call__(self, x, pos=None): """ @@ -947,7 +947,6 @@ def __call__(self, x, pos=None): is_x_decade = is_close_to_int(fx) exponent = np.round(fx) if is_x_decade else np.floor(fx) coeff = np.round(x / b ** exponent) - if self.labelOnlyBase and not is_x_decade: return '' if self._sublabels is not None and coeff not in self._sublabels: @@ -2172,7 +2171,7 @@ def get_log_range(lo, hi): def view_limits(self, vmin, vmax): 'Try to choose the view limits intelligently' - b = self._transform.base + b = self._base if vmax < vmin: vmin, vmax = vmax, vmin From 37fcaf2594e4e9ca8cecdfb119f03e0c721f0633 Mon Sep 17 00:00:00 2001 From: Nelle Varoquaux Date: Wed, 23 Nov 2016 15:52:27 -0800 Subject: [PATCH 14/15] DOC avoid references to decades in ticker module --- lib/matplotlib/ticker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index a91f66d80d66..207425ceb9c7 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -877,7 +877,7 @@ def label_minor(self, labelOnlyBase): Parameters ---------- labelOnlyBase : bool, optional, default: True - If True, only label decades. + If true, label ticks only at integer powers of base. """ self.labelOnlyBase = labelOnlyBase From ecc62126e1dc2005dc47e1f8245628d58522b4cc Mon Sep 17 00:00:00 2001 From: Eric Firing Date: Thu, 24 Nov 2016 22:22:29 -1000 Subject: [PATCH 15/15] Fix docstring; validate subs --- lib/matplotlib/ticker.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 207425ceb9c7..6bb383e8ee8c 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -876,8 +876,8 @@ def label_minor(self, labelOnlyBase): Parameters ---------- - labelOnlyBase : bool, optional, default: True - If true, label ticks only at integer powers of base. + labelOnlyBase : bool + If True, label ticks only at integer powers of base. """ self.labelOnlyBase = labelOnlyBase @@ -1908,7 +1908,9 @@ def subs(self, subs): if subs is None: # consistency with previous bad API self._subs = 'auto' elif cbook.is_string_like(subs): - # TODO: validation ('all', 'auto') + if subs not in ('all', 'auto'): + raise ValueError("A subs string must be 'all' or 'auto'; " + "found '%s'." % subs) self._subs = subs else: self._subs = np.asarray(subs, dtype=float) @@ -2045,8 +2047,8 @@ def __init__(self, transform=None, subs=None, linthresh=None, base=None): self._base = base self._linthresh = linthresh else: - raise ValueError("Either transform or linthresh " - "and base must be provided.") + raise ValueError("Either transform, or both linthresh " + "and base, must be provided.") if subs is None: self._subs = [1.0] else: