diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 7193d1e7edf6..04dea31e2177 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -8,7 +8,7 @@ from matplotlib.cbook import dedent from matplotlib.ticker import (NullFormatter, ScalarFormatter, - LogFormatterMathtext, LogitFormatter) + LogFormatterSciNotation, LogitFormatter) from matplotlib.ticker import (NullLocator, LogLocator, AutoLocator, SymmetricalLogLocator, LogitLocator) from matplotlib.transforms import Transform, IdentityTransform @@ -304,9 +304,9 @@ def set_default_locators_and_formatters(self, axis): log scaling. """ axis.set_major_locator(LogLocator(self.base)) - axis.set_major_formatter(LogFormatterMathtext(self.base)) + axis.set_major_formatter(LogFormatterSciNotation(self.base)) axis.set_minor_locator(LogLocator(self.base, self.subs)) - axis.set_minor_formatter(NullFormatter()) + axis.set_minor_formatter(LogFormatterSciNotation(self.base)) def get_transform(self): """ @@ -462,7 +462,7 @@ def set_default_locators_and_formatters(self, axis): symmetrical log scaling. """ axis.set_major_locator(SymmetricalLogLocator(self.get_transform())) - axis.set_major_formatter(LogFormatterMathtext(self.base)) + axis.set_major_formatter(LogFormatterSciNotation(self.base)) axis.set_minor_locator(SymmetricalLogLocator(self.get_transform(), self.subs)) axis.set_minor_formatter(NullFormatter()) diff --git a/lib/matplotlib/tests/baseline_images/test_axes/log_scales.pdf b/lib/matplotlib/tests/baseline_images/test_axes/log_scales.pdf index c76b653c33fb..03284a4e4ee0 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/log_scales.pdf and b/lib/matplotlib/tests/baseline_images/test_axes/log_scales.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/log_scales.png b/lib/matplotlib/tests/baseline_images/test_axes/log_scales.png index 876b47caedb6..92d8776d6cc4 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/log_scales.png and b/lib/matplotlib/tests/baseline_images/test_axes/log_scales.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/log_scales.svg b/lib/matplotlib/tests/baseline_images/test_axes/log_scales.svg index 2e63e7be1870..c6efd3710792 100644 --- a/lib/matplotlib/tests/baseline_images/test_axes/log_scales.svg +++ b/lib/matplotlib/tests/baseline_images/test_axes/log_scales.svg @@ -27,7 +27,7 @@ z " style="fill:#ffffff;"/> - +" id="m2d4ddba423" style="stroke:#000000;stroke-width:0.5;"/> - + +" id="m4e4fc712fc" style="stroke:#000000;stroke-width:0.5;"/> - + @@ -181,12 +181,12 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + @@ -215,12 +215,12 @@ z - + - + @@ -261,176 +261,241 @@ Q 31.109375 20.453125 19.1875 8.296875 +" id="m1acb1eab51" style="stroke:#000000;stroke-width:0.5;"/> - + +" id="mb4079269e1" style="stroke:#000000;stroke-width:0.5;"/> - + - + - + + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + - + - + - + - + - + - + - + - + - + - + @@ -441,23 +506,23 @@ L 0 2 +" id="md4f0ea9b24" style="stroke:#000000;stroke-width:0.5;"/> - + +" id="mb109f2c327" style="stroke:#000000;stroke-width:0.5;"/> - + - + - + - + - + @@ -530,15 +595,15 @@ z - + - + - + @@ -553,104 +618,127 @@ z +" id="m6ccf459a85" style="stroke:#000000;stroke-width:0.5;"/> - + +" id="m4cdb1537b0" style="stroke:#000000;stroke-width:0.5;"/> - + + + + + + + + + + + + + - + - + - + - + - + - + - + - + + + + + + + + + + + + - + - + - + - + - + - + @@ -658,7 +746,7 @@ L -2 0 - + diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 59deeaef892a..8a4833d5ee40 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -207,6 +207,48 @@ def check_offset_for(left, right, offset): yield check_offset_for, right, left, offset +def _sub_labels(axis, subs=()): + "Test whether locator marks subs to be labeled" + fmt = axis.get_minor_formatter() + minor_tlocs = axis.get_minorticklocs() + fmt.set_locs(minor_tlocs) + coefs = minor_tlocs / 10**(np.floor(np.log10(minor_tlocs))) + label_expected = [np.round(c) in subs for c in coefs] + label_test = [fmt(x) != '' for x in minor_tlocs] + assert_equal(label_test, label_expected) + + +@cleanup +def test_LogFormatter_sublabel(): + # test label locator + fig, ax = plt.subplots() + ax.set_xscale('log') + 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)) + # axis range above 3 decades, only bases are labeled + ax.set_xlim(1, 1e4) + fmt = ax.xaxis.get_major_formatter() + fmt.set_locs(ax.xaxis.get_majorticklocs()) + show_major_labels = [fmt(x) != '' for x in ax.xaxis.get_majorticklocs()] + assert np.all(show_major_labels) + _sub_labels(ax.xaxis, subs=[]) + + # axis range at 2 to 3 decades, label sub 3 + ax.set_xlim(1, 800) + _sub_labels(ax.xaxis, subs=[3]) + + # axis range at 1 to 2 decades, label subs 2 and 5 + ax.set_xlim(1, 80) + _sub_labels(ax.xaxis, subs=[2, 5]) + + # axis range at 0 to 1 decades, label subs 2, 3, 6 + ax.set_xlim(1, 8) + _sub_labels(ax.xaxis, subs=[2, 3, 6]) + + def _logfe_helper(formatter, base, locs, i, expected_result): vals = base**locs labels = [formatter(x, pos) for (x, pos) in zip(vals, i)] @@ -252,6 +294,37 @@ def get_view_interval(self): yield _logfe_helper, formatter, base, locs, i, expected_result +def test_LogFormatterSciNotation(): + test_cases = { + 10: ( + (1e-05, '${10^{-5}}$'), + (1, '${10^{0}}$'), + (100000, '${10^{5}}$'), + (2e-05, '${2\\times10^{-5}}$'), + (2, '${2\\times10^{0}}$'), + (200000, '${2\\times10^{5}}$'), + (5e-05, '${5\\times10^{-5}}$'), + (5, '${5\\times10^{0}}$'), + (500000, '${5\\times10^{5}}$'), + ), + 2: ( + (0.03125, '${2^{-5}}$'), + (1, '${2^{0}}$'), + (32, '${2^{5}}$'), + (0.0375, '${1.2\\times2^{-5}}$'), + (1.2, '${1.2\\times2^{0}}$'), + (38.4, '${1.2\\times2^{5}}$'), + ) + } + + for base in test_cases.keys(): + formatter = mticker.LogFormatterSciNotation(base=base) + formatter.sublabel = set([1, 2, 5, 1.2]) + for value, expected in test_cases[base]: + with matplotlib.rc_context({'text.usetex': False}): + nose.tools.assert_equal(formatter(value), expected) + + def _pprint_helper(value, domain, expected): fmt = mticker.LogFormatter() label = fmt.pprint_val(value, domain) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index a2be7cbd805e..4bd1a36148dc 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -820,6 +820,7 @@ def __init__(self, base=10.0, labelOnlyBase=True): """ self._base = base + 0.0 self.labelOnlyBase = labelOnlyBase + self.sublabel = [1, ] def base(self, base): """ @@ -839,12 +840,46 @@ def label_minor(self, labelOnlyBase): """ self.labelOnlyBase = labelOnlyBase + def set_locs(self, locs): + 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'): + t = self.axis.get_transform() + linthresh = t.linthresh + # 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) + else: + vmin = math.log(vmin) / math.log(b) + vmax = math.log(vmax) / math.log(b) + numdec = abs(vmax - vmin) + + if numdec > 3: + # Label only bases + self.sublabel = 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)) + def __call__(self, x, pos=None): """ Return the format for tick val `x` at position `pos`. """ - vmin, vmax = self.axis.get_view_interval() - d = abs(vmax - vmin) b = self._base if x == 0.0: return '0' @@ -852,16 +887,21 @@ def __call__(self, x, pos=None): # only label the decades fx = math.log(abs(x)) / math.log(b) isDecade = is_close_to_int(fx) - if not isDecade and self.labelOnlyBase: - s = '' - elif x > 10000: - s = '%1.0e' % x - elif x < 1: - s = '%1.0e' % x + 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: + s = '' + 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 else: - s = self.pprint_val(x, d) - if sign == -1: - s = '-%s' % s + s = '' return self.fix_minus(s) @@ -951,6 +991,14 @@ class LogFormatterMathtext(LogFormatter): Format values for log axis using ``exponent = log_base(value)``. """ + def _non_decade_format(self, sign_string, base, fx, usetex): + 'Return string for non-decade locations' + if usetex: + return (r'$%s%s^{%.2f}$') % (sign_string, base, fx) + else: + return ('$%s$' % _mathdefault('%s%s^{%.2f}' % + (sign_string, base, fx))) + def __call__(self, x, pos=None): """ Return the format for tick value `x`. @@ -969,6 +1017,8 @@ def __call__(self, x, pos=None): 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) + coeff = np.round(x / b ** exponent) sign_string = '-' if x < 0 else '' @@ -978,25 +1028,46 @@ def __call__(self, x, pos=None): else: base = '%s' % b - if not is_decade and self.labelOnlyBase: - return '' - elif not is_decade: - if usetex: - return (r'$%s%s^{%.2f}$') % \ - (sign_string, base, fx) + 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: - return ('$%s$' % _mathdefault( - '%s%s^{%.2f}' % - (sign_string, base, fx))) + 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 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)))) + return '' + + +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) + exponent = math.floor(fx) + coeff = b ** fx / b ** exponent + if is_close_to_int(coeff): + coeff = nearest_long(coeff) + if usetex: + return (r'$%g\times%s^{%d}$') % \ + (coeff, base, exponent) + else: + return ('$%s$' % _mathdefault(r'%g\times%s^{%d}' % + (coeff, base, exponent))) class LogitFormatter(Formatter):